题目
在游戏《星际争霸 II》中,高阶圣堂武士作为星灵的重要 AOE 单位,在 游戏的中后期发挥着重要的作用,其技能”灵能风暴“可以消耗大量的灵能对 一片区域内的敌军造成毁灭性的伤害。经常用于对抗人类的生化部队和虫族的 刺蛇飞龙等低血量单位。
你控制着 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 点灵能。形 式化来讲就是 ai−1+ = ai, ai+1+ = ai, ai− = 2ai。
灵能是非常高效的作战工具,同时也非常危险且不稳定,一位高阶圣堂武士拥有的灵能过多或者过少都不好,定义一组高阶圣堂武士的不稳定度为
m
a
x
n
i
=
1
∣
a
i
∣
{max}_{n}^{i=1}|a_i|
maxni=1∣ai∣ ,请你通过不限次数的传递灵能操作使得你控制的这一组高阶圣堂武 i=1士的不稳定度最小。
输入
本题包含多组询问。输入的第一行包含一个正整数 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
解题思路
本题可以DFS列举,但时间肯定会超限。于是,看到一维数列,想到了采用前缀和,对于给定的数列a1—an,其前缀和可以表示为s[1]-s[0]、s[2]-s[1]…s[n]-s[n-1](其中s[0]为0),这样,题目要求的 m a x n i = 1 ∣ a i ∣ {max}_{n}^{i=1}|a_i| maxni=1∣ai∣ 就转化为了求前缀和相邻两项之差的最大绝对值的最小值。
再来看题目中规定的变换:ai−1+ = ai, ai+1+ = ai, ai−=2ai,反映到前缀和即为:si-1+=ai,si-=ai,因此,如果发生题中所述的变换,即为将原来的si-1和si的值进行交换(使用前缀和之差的好处得以显现,交换操作被简化),而交换并不影响si+1的值。
最后,要求“前缀和相邻两项之差的最大绝对值的最小值”:由于s[0]始终为0不可更改,s[n]又是一个定值,不可交换(因为a[n-1]是最后一个“可选择的高阶圣堂武士”),故本题转变为一个“s[0]、s[n]为定值,s[1]-s[n-1]为已知,通过交换s[1]-s[n-1],使得数组s相邻两个数之间差的绝对值最小的问题”。
显然,如果是一个数列a,所有元素都可以自由交换,则升序或者降序排列可使得相邻元素之差绝对值最小;借鉴此思路,我们假设s[0] = min(s[0],s[n]),s[n] = max(s[0],s[n]),那么在s数列的s[0]、s[n]同样可以视为升序数列中的两个点,但原点和终点的位置在数列中间某处,s中的其余数值按照升序排列放入其中,如下图所示:
但我们会发现,原点和终点之间的差值一定会很大,这显然不符合我们要求,所以想到“交叉放置”升序数列中的s[i](i!=0且i!=n)(没有数学推理)。比如,n=9, s[10] = {0,1,-3,2,-6,-1,5,8,-4,4},由于s[0]<s[n],因此选择升序排列,排列后得到{-6,-4,-3,-1,0,1,2,4,5,8}。为了便于直观感受,我画出了最终调换后的数组,如下图所示:
根据实际给定数据的不同,需要注意s[0]和s[n]的大小关系,当前者大于后者时,对s进行降序排列;反之,升序排列。还需要注意最后可能不一定能“跳回”s[n],因此,若恰好无法跳回,注意补充给s[n]赋值。
(参考视频:https://www.bilibili.com/video/BV1Mb411t7M1?share_source=copy_web,感谢分享思路!)
易错点
- Lmax函数中,“跳着取”s[i]的值的下标需要注意,以及跳出循环的条件需要注意;
- 求出的前缀和可能很大,因此需要以long int来存储,相应的,qsort当中的cmp函数也需要重写,不可以将long int类型直接相减的值作为返回值,否则将排序错误。
代码
#include<stdio.h>
#include<stdlib.h>
int cmp_up(const void *a, const void *b){
long int c = *(long int *)a;
long int d = *(long int *)b;
if (c>d)//升序
return 1;
else
return -1;
}
int cmp_d(const void *a, const void *b){
long int c = *(long int *)a;
long int d = *(long int *)b;
if (c<d)//降序
return 1;
else
return -1;
}
long int int_abs(long int a){//返回long int类型的绝对值
return (a>=0)?a:-a;
}
long int Lmax(int n, long int s[],int sub_s0, int sub_sn){
int i,k=1;
long int max=0,temp=0;
long int a[n+1];
a[0] = 0;//避免0是最小数,a[0]未赋值的情况
a[n] = s[sub_sn];
//printf("%d %d",sub_s0,sub_sn);
for (i=sub_s0-2;i>=0;i-=2)//从s0到s_min/s_max
{
a[k++] = s[i];
if (i<=1)
break;
}
for (i = (i==0)?1:0;i<sub_s0;i+=2)//从s_min到s_0
{
a[k++] = s[i];
if ((i+2)>=sub_s0)
break;
}
for (i = sub_s0+1;i<sub_sn;i++)//中间部分
a[k++] = s[i];
for (i = sub_sn+1;i<=n;i+=2)//从sn后面的第一个到末尾
{
a[k++] = s[i];
if((i+2)>n)
break;
}
for (i = (i==n)?(n-1):n;i>=sub_sn;i-=2)//从smax回到sn
{
a[k++] = s[i];
if ((i-2)<sub_sn)
break;
}
for (i=1;i<=n;i++)//遍历查找最大值
{
temp = int_abs(a[i]-a[i-1]);
//printf("%ld",temp);
if (temp>max)
max = temp;
}
return max;
}
int main()
{
int T,i,n,j,sub_s0,sub_sn;
long int sn;
scanf("%d",&T);
for (i=0;i<T;i++)
{
scanf("%d",&n);
long int a[n+1],s[n+1];//累加和可能是long int
sub_s0=-1;
sub_sn=-1;
s[0] = 0;//为了便于求第一项前缀和
for (j=1;j<=n;j++)
{
scanf("%ld",&a[j]);
s[j] = s[j-1]+a[j];//求前缀和
}
sn = s[n];//记录下sn的值,便于之后查找
if (sn>=0)//如果sn>s0,那么升序排列
qsort(s,n+1,sizeof(long int),cmp_up);
else//反之,降序排列
qsort(s,n+1,sizeof(long int),cmp_d);
for (j=0;j<=n;j++)//sub_sn>sub_s0
{
if (sub_sn==-1 && s[j]==sn)
sub_sn = j;
else if (sub_s0==-1 && s[j]==0)
sub_s0 = j;
if (sub_s0>-1 && sub_sn>-1)//找到了下标
break;
}
printf("%ld\n",Lmax(n,s,sub_s0,sub_sn));
}
return 0;
}
错误代码
以下代码是错误代码(43分),主要问题是求前缀和的数列没有用long int类型,且Lmax函数中的“跳着取”的边界条件错误。
#include<stdio.h>
#include<stdlib.h>
int cmp_up(const void *a, const void *b){
return *(int *)a - *(int *)b;//升序
}
int cmp_d(const void *a, const void *b){
return *(int *)b - *(int *)a;//降序
}
int int_abs(int a){
return (a>=0)?a:-a;
}
int Lmax(int n, int s[],int sub_s0, int sub_sn){
int i,k=1,max=0,temp=0;
int a[n+1];
a[0] = 0;//避免0是最小数,a[0]未赋值的情况
for (i=sub_s0-2;i>1;i-=2)//从s0到s_min/s_max
a[k++] = s[i];
for (i = (i==0)?1:0;i<(sub_s0-2);i+=2)//从s_min到s_0
a[k++] = s[i];
for (i = sub_s0+1;i<sub_sn;i++)//中间部分
a[k++] = s[i];
for (i = sub_sn+1;i<=(n-2);i+=2)//从sn后面的第一个到末尾
a[k++] = s[i];
for (i = (i==n)?(n-1):n;i>=(sub_sn+2);i-=2)
a[k++] = s[i];
a[n] = s[sub_sn];
for (i=1;i<=n;i++)
{
temp = int_abs(a[i]-a[i-1]);
if (temp>max)
max = temp;
}
return max;
}
int main()
{
int T,i,n,j,sub_s0,sub_sn;
int sn;
scanf("%d",&T);
for (i=0;i<T;i++)
{
scanf("%d",&n);
int a[n+1],s[n+1];
sub_s0=-1;
sub_sn=-1;
s[0] = 0;//为了便于求第一项前缀和
for (j=1;j<=n;j++)
{
scanf("%d",&a[j]);
s[j] = s[j-1]+a[j];//求前缀和
}
sn = s[n];//记录下sn的值,便于之后查找
if (sn>0)
qsort(s,n+1,sizeof(int),cmp_up);
else
qsort(s,n+1,sizeof(int),cmp_d);
for (j=0;j<=n;j++)//sub_sn>sub_s0
{
if (sub_sn==-1 && s[j]==sn)
sub_sn = j;
else if (sub_s0==-1 && s[j]==0)
sub_s0 = j;
if (sub_s0>-1 && sub_sn>-1)//找到了下标
break;
}
printf("%d\n",Lmax(n,s,sub_s0,sub_sn));
}
return 0;
}