题目 2307: 蓝桥杯2019年第十届省赛真题-灵能传输

题目

在游戏《星际争霸 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=1ai ,请你通过不限次数的传递灵能操作使得你控制的这一组高阶圣堂武 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=1ai 就转化为了求前缀和相邻两项之差的最大绝对值的最小值

再来看题目中规定的变换: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,感谢分享思路!)

易错点

  1. Lmax函数中,“跳着取”s[i]的值的下标需要注意,以及跳出循环的条件需要注意;
  2. 求出的前缀和可能很大,因此需要以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;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值