蓝桥杯精选赛题系列——灵能传输——前缀和的应用

已收录此专栏。.

前缀和的引入

1.前缀和是什么呢?
对于一个长度为n的数组a[0]∼a[n−1],它的前缀和 sum[i] 等于 a[0]∼a[i] 的和。例如:
sum[0] = a[0]
sum[1] = a[0] + a[1]
sum[2] = a[0] + a[1] + a[2]

我们可以用递推,也可以计算出sum[i],例如:sum[i]=sum[i−1]+a[i]。当然,我们也能用 sum[]反推计算出 a[]:a[i]=sum[i]−sum[i−1]。
那如果预先算出了前缀和,这时候想知道a[i]∼a[j] 的和,我可以怎么计算?
和上面 a[i]=sum[i]−sum[i−1] 类似,a[i] + a[i+1] + … + a[j-1] + a[j] = sum[j]-sum[i-1]。
接下来是一道前缀和的应用妙题。

题目描述

你控制着 n 名高阶圣堂武士,方便起见标为 1,2,⋅⋅⋅,n。每名高阶圣堂武士需要一定的灵能来战斗,每个人有一个灵能值 ai表示其拥有的灵能的多少,ai非负表示这名高阶圣堂武士比在最佳状态下多余了 ai点灵能,ai为负则表示这名高阶圣堂武士还需要 −ai 点灵能才能到达最佳战斗状态)。

现在系统赋予了你的高阶圣堂武士一个能力,传递灵能,每次你可以选择一个 i∈[2,n−1]​,若 ai≥ 0 则其两旁的高阶圣堂武士,也就是i−1、i+1​ 这两名高阶圣堂武士会从 i​ 这名高阶圣堂武士这里各抽取 ai点灵能;若 ai<0​ 则其两旁的高阶圣堂武士,也就是i−1,i+1​ 这两名高阶圣堂武士会给 i 这名高阶圣堂武士 −ai
​点灵能。形式化来讲就是 a{i−1}+ = ai,a{i+1}+ = ai,ai−=2ai。

灵能是非常高效的作战工具,同时也非常危险且不稳定,一位高阶圣堂武士拥有的灵能过多或者过少都不好,定义一组高阶圣堂武士的不稳定为maxx{i=1}|ai|,请你通过不限次数的传递灵能操作使得你控制的这一组高阶圣堂武士的不稳定度最小。

输入描述

本题包含多组询问。

输入的第一行包含一个正整数 T 表示询问组数。 接下来依次输入每一组询问。

每组询问的第一行包含一个正整数 n,表示高阶圣堂武士的数量。

接下来一行包含 n 个数 a1,a2,··· ,an。

其中,T≤3,3≤n≤300000,∣ai∣≤109

输出描述

输出 T 行。每行一个整数依次表示每组询问的答案。

样例输入

3
3
5 -2 3
4
0 0 0 0
3
1 2 3

样例输出

3
0
3

解题思路

我先简化一下题意吧。题目的意思是:给定一个数组 a[]​​,一次操作是对连续的 3个数做加减,经过多次操作后得到的数组,其中有一个数的绝对值最大;问这个最大的绝对值能达到多小。 举个例子:a[]​​ = {5, -2, 3},这里让下标从 1​ 开始,即 a[1]=5,a[2]=-2,a[3]=3​。经过以下变换:

a[1] += a[2],即 a[1] = a[1]+a[2] = 5-2 = 3
a[3] += a[2],即 a[3] = a[3]+a[2] = 3-2 = 1
a[2] -= 2a[2],即 a[2] = a[2]-2a[2] = -2+4 = 2
得 a[] = {3, 2, 1},答案为 3​。

所有加减操作都是在数组内部进行,也就是说对于整个数组的和不会有影响;
一次操作是对连续的 3​​​​​​​ 个数 a[i−1]​​​​​​、a[i]​​​​​、a[i+1]​​​​,根据 a[i-1] += a[i],a[i+1] += a[i],a[i] = -2a[i],计算得前缀和 s[i+1]​​的值不变。因为这些数的加减都是在 a[i-1]、a[i]​、a[i+1] 内部进行的,并且三个数的和不变。

再来分析一次操作后的前缀和:

a[i-1]​​​ 更新为a[i]+a[i−1]​​,那么s[i−1]​ 的新值等于原来的 s[i];
a[i]更新为−2a[i],那么s[i] 的新值等于原来的s[i−1];
a[i+1] 更新为 a[i] + a[i+1], s[i+1] 的值保持不变。

s[i]​​​​​ 和s[i−1]​​​​​ 互相交换,s[i+1]​​​​​ 不变。而s[i−1]​​​​​、s[i]​​​​​、s[i+1]​​​​​ 这 3​​​​​ 个数值还在,没有出现新的数值。设 a[0]=0​​​​​,观察前缀和数组s[0]​​​​​、s[1]​​​​​、s[2]​​​​​、⋯​​​​​、s[n−1]​​​​​、s[n]​​​​​。除了s[0]​​​​、s[n]​​​ 外,其他的 s[1]​、s[2]、⋯、s[n−1],经过多次操作后,每个s[i] 能到达任意位置。
也就是说,题目中对 a[] 的多次操作后的一个结果,对应了前缀和 s[] 的一种排列。
因为a[i]=s[i]−s[i−1],对a[] 多次操作后的结果是:
a[1]=s[1]−s[0],a[2]=s[2]−s[1],⋯,a[n]=s[n]−s[n−1]
经过以上转换,题目的原意“对连续 3 个数做加减操作后,求最大的 a[] 能达到多小”,变成了比较简单的问题“数组 s[],
求max​​{∣s[1]−s[0]∣,∣s[2]−s[1]∣,⋯,∣s[n]−s[n−1]∣​​}”。而根据题目的要求,s[0]​ 和 s[n]是保持不动的,其他 s[]​​ 可以随意变换位置。

我们先看一个特殊情况:
1.s[0]是最小的,s[n] 是最大的。 那么简单了,我们把 s[] 排序后,max{|s[i]-s[i-1]|}就是解了。
请添加图片描述
2.s[0] 不是最小的,s[n] 不是最大的。 这种情况比较麻烦。先把 s[] 排序,s[0] 和 s[n] 在中间某两个位置,见下图:
请添加图片描述
此时应该从 s[0] 出发,到最小值 min,然后到最大值 max,最后到达 s[n],如图所示路线 1→2→3,这样产生的 |s[i]−s[i−1]| 会比较小。
那如果图中存在重叠区,即 [min,s0] 和 [sn ,max]上有重叠该怎么办呢?例如在 [min,s0]上来回走了两遍,但是这区间的每个数只能用一次。那就隔一个数取一个。
如何判断重叠区呢?
用vis[i]=1 记录第一次走过的时候第 i 数被取过,第二次再走过时,vis[]=1 的数就不用再取了。

详细解答

T=int(input())
for p in range(T):
    n=int(input())
    a=list(map(int,input().split()))
    s=[0]+a
    for i in range(1,n+1):
        s[i]+=s[i-1]
    sn=s[n]
    s.sort()
    a=[0 for i in range(n+1)]
    s0=0
    if s0>sn:
        sn,s0=s0,sn
    for i in range(n+1):
        if s[i]==s0:
            s0=i
            break
    for i in range(n,-1,-1):
        if s[i]==sn:
            sn=i
            break
    l=0
    r=n
    a[n]=s[n]
    vis=[True for i in range(n+1)]
    for i in range(s0,-1,-2):
        a[l]=s[i]
        vis[i]=False
        l+=1
    for i in range(sn,n+1,2):
        a[r]=s[i]
        vis[i]=False
        r-=1
    for i in range(n+1):
        if vis[i]:
            a[l]=s[i]
            l+=1
    res=0
    for i in range(n):
        res=max(res,abs(a[i+1]-a[i]))
    print(res)

猜你喜欢

转载自blog.csdn.net/m0_51951121/article/details/122721343