Codeforces Round #547 C. Polycarp Restores Permutation(二分枚举/数学+模拟)

  • Post author:
  • Post category:其他


在这里插入图片描述


题意: 有一个长度为N的序列p,该序列保证存在1~N每个值都存在,现在给出一个序列q,长度为N-1,表示序列p相邻两数之差,根据序列q输出序列p


题解:

一开始想复杂了,以为要找什么最大差值最小差值来扩展,又或者枚举的话可能因为不断尝试的过程需要递归搜索,复杂度会很大。。。。

其实非常简单,想复杂了自己吓自己了,首先可以明确的是,给出了该序列的相邻两值差值,那么直接枚举第一个值就能计算出整个序列,再计算的过程中判断是否合法即可。而题目中给出的条件1~N必须存在的作用就是给出了第一个值的枚举范围,若N=4,第一个差值是-3,那么第一个值绝对不可能小于4,因为在N中-3已经达到了最大值和最小值之间的差值,即4和1,那么我们只需枚举4即可通过加减差值得到整个序列,并且这个序列是唯一的,复杂度O(n)。

大部分情况下给出的差值将限定第一个值的合法枚举范围,只需遍历这个范围,不断计算每一个可能的唯一序列,并判断是否合法即可。

可以知道,若差值的第一个数是q[0],若q[0]为负数,那么这个枚举范围即:

1-q[0] ——–> N


否则枚举范围是

q[0]+1 ———–> n-q[0]

一开始用for循环枚举判断每个值,这个复杂度可能是O(N*N)的,最复杂情况,当2e5个差值是等差序列,差值为1,但是最后一个值的差值存在巨大异常,那么枚举的范围将是2e5,而得到不合法序列的时间将是计算到最后才知道。【第41组case】

考虑两个优化,首先我们知道,在差值确定的情况下,当我们重复计算出某个值的时候,无论枚举任何数,都会出现重复值,因为题意是必须出现1~N所有值,因此不可能出现重复值,一旦出现重复值,就不继续构造该序列,直接无解。

第二个也是最重点的优化,即for枚举改成二分枚举,因为可以知道,当计算一个值出现不合法情况是,如计算出来的值是大于N的或小于0的,可以根据结果调整枚举值的范围,想象该序列是一段折线,那么当某个值大于N时,我们将枚举的第一个值向下调整,当小于0时,第一个值向上调整,那么整个序列都将上移,二分枚举不断找到一个令所有值都合法的序列

在这里插入图片描述

最后复杂度,O(N*logN)

代码如下:

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn=2e5+7;
int n,q[maxn];
bool vis[maxn<<1];
vector<int>p;
int main()
{
    scanf("%d",&n);
    bool flag=false,gg=false;
    for(int i=1; i<n; i++)scanf("%d",&q[i]);
    int l,r;
    if(q[0]>0) r=n-q[0],l=q[0]+1;//二分边界
    else l=1-q[0],r=n;
    while(l<=r)
    {
        int i=(l+r)/2,tmp;
        memset(vis,false,sizeof vis);
        vis[i]=true;
        p.push_back(i);
        for(int j=1; j<n; j++)
        {
            tmp=p.back()+q[j];
            if(vis[tmp]==false&&tmp>0&&tmp<=n)
            {
                vis[p.back()+q[j]]=true;
                p.push_back(p.back()+q[j]);
                if(p.size()==n)flag=true;
            }
            else
            {
                if(tmp>0&&vis[tmp]==true)gg=true;
                break;
            }
        }
        if(flag||gg)break;
        p.clear();
        if(tmp>n)r=i-1;
        else l=i+1;
    }
    if(gg)//相同值时无解
    {
        printf("-1\n");
        return 0;
    }
    if(flag)
        for(int i=0; i<p.size(); i++)printf("%d%c",p[i],i==p.size()?'\n':' ');
    else printf("-1\n");
}


后来突然发现该题标签是math【数学题】


感谢大佬

@satomiyo

提供数学算法思路

首先对于原序列a有:


a1 a2 a3 a4 a5 a6 a7 …an

对于差值序列x有:


x1 x2 x3 x4 x5 x6 …x(n-1)

我们对差值序列求和可以得到


y1 y2 y3 y4 y5 y6 …y(n-1)

该差值的求和可以表示,an – a1=y(n-1)



a4 – a1 = y3

接着我们列出式子:


a1 +(a2-a1) +(a3-a1) +(a4-a1) +(a5-a1) +(a6-a1) +…+(an-a1)

其中括号内的每一个值即y1,y2,y3,y4…

那么经过整理后将所有正数提出来,将所有-a1整合在一起,用等差数列求和公式计算a1+…+an,得到式子:


n × (n+1) / 2 – (n-1) × a1 = a1 + (y1 + …+ y(n-1))

移项,并对y1~y(n-1)求和为sum后得到


n × (n+1) / 2 – n × a1 = sum

这个式子中只有a1是未知的,

a1=( n × (n+1) / 2 -sum)/n

最后拿这个确定的a1计算整个式子的值,输出即可,注意计算式子时仍要判断是否符合条件【也可能重复】,不符合输出-1。并且a1的计算结果也可能不合法,即直接输出-1

代码如下:

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn=2e5+7;
LL n;
LL q[maxn];
LL ss[maxn];
bool vis[maxn];
vector<LL>p;
int main()
{
    scanf("%lld",&n);
    LL sum=n*(n+1)/2;
    for(int i=1; i<n; i++)
    {
        scanf("%lld",&q[i]);
        ss[i]=ss[i-1]+q[i];
    }
    for(int i=1; i<n; i++)ss[i]+=ss[i-1];
    LL num=sum-ss[n-1];
    bool flag=false;
    if(num%n==0)
    {
        LL a1=num/n;
        if(a1>0&&a1<=n)
        {
            for(int i=0; i<n; i++)
            {
                if(a1+q[i]>0&&a1+q[i]<=n&&vis[a1+q[i]]==false)
                {
                    a1+=q[i];
                    p.push_back(a1);
                    vis[a1]=true;
                }
                else
                {
                    flag=true;
                    printf("-1\n");
                    break;
                }
            }
            if(!flag)
                for(int i=0; i<n; i++)
                    printf("%lld%c",p[i],i==n-1?'\n':' ');

        }
        else printf("-1\n");
    }
    else printf("-1\n");
}



版权声明:本文为kuronekonano原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。