GYM100702 D

来源:互联网 发布:适合游戏编程的电脑 编辑:程序博客网 时间:2024/06/06 18:42

题意:
有一个n个数字的可重集合,给出2n个子集的和,最多10000种不同的值。问排序后字典序最小原集合。
n<=60

#include<cstring>#include<cstdlib>#include<cstdio>#include<cmath>#include<iostream>#include<algorithm>#define N 11000#define LL long longusing namespace std;struct node{LL d,f;}a[N],b[N],c[N],r[N];LL ans[N];int n,m,num;LL gcd(LL a,LL b){    if(b==0) return a;    return gcd(b,a%b);}int find(LL t){    int l=1,r=n;    while(l<=r)    {        int mid=(l+r)/2;        if(a[mid].d==t) return mid;        if(a[mid].d>t) r=mid-1;        else l=mid+1;    }    return -1;}bool check(LL t){    for(int i=1;i<=n;i++) b[i]=c[i]=a[i],c[i].f=0;    if(t>0)    {        for(int i=n;i>=1;i--)        {            if(b[i].f==0) continue;            int k=find(b[i].d-t);            if(k==-1 || b[i].f>b[k].f) return 0;            b[k].f-=b[i].f;c[k].f+=b[i].f;        }    }    else    {        for(int i=1;i<=n;i++)        {            if(b[i].f==0) continue;            int k=find(b[i].d-t);            if(k==-1 || b[i].f>b[k].f) return 0;            b[k].f-=b[i].f;c[k].f+=b[i].f;        }    }    for(int i=1;i<=n;i++) if(c[i].d==0 && c[i].f==0) return 0;    return 1;}int main(){    int z,zu=0;scanf("%d",&z);    while(z--)    {        scanf("%d",&n);        for(int i=1;i<=n;i++) scanf("%lld",&a[i].d);        for(int i=1;i<=n;i++) scanf("%lld",&a[i].f);        LL g=a[1].f;        for(int i=1;i<=n;i++) g=gcd(g,a[i].f);        int tmp=0,tn=n;        while(g%2==0) g/=2,tmp++;        for(int i=1;i<=n;i++) a[i].f/=1ll<<tmp;        for(int i=1;i<=n;i++) r[i]=a[i];        num=0;        while(n>1)        {            LL x=a[2].d-a[1].d;            check(x);            m=0;            for(int i=1;i<=n;i++) if(c[i].f) a[++m]=c[i];            n=m;            ans[++num]=x;        }        sort(ans+1,ans+num+1);        n=tn;        for(int i=1;i<=n;i++) a[i]=r[i];        for(int i=num;i>=1;i--)        {            if(check(-ans[i])==0) check(ans[i]);            else ans[i]=-ans[i];            m=0;            for(int i=1;i<=n;i++) if(c[i].f) a[++m]=c[i];            n=m;        }        while(tmp) ans[++num]=0,tmp--;        sort(ans+1,ans+num+1);        printf("Case #%d: ",++zu);        for(int i=1;i<=num;i++) printf("%lld ",ans[i]);        printf("\n");    }    return 0;}

题解:
首先,最小的负值出现次数是2p,代表有p个0。
证明:
假设把所有0拿走,如果拿走了k个0,那所有值的出现次数都要除2k。在没有0的情况下,构成最小值的方法只有一种,就是所有负数的和,故最小负值出现次数就是2k

先把所有0拿走
设最小负值和次小负值的差为d。最小负值一定是所有负数的和,次小负值要么是最小负值减最大负数,要么是最小负值加最小正数,说明d和-d一定存在一个。

然后我就不知道该拿哪一个,看了一眼题解

题解说,不管拿哪一个,本质上是没有区别的,意思就是最终拿出来的所有数的绝对值的集合是一样的。

脑补一下确实是这样的,考虑拿走一个数d之后,要把所有值分成两个集合,包含d的集合里所有的值减d后和不包含d的集合相等。
对于两个值x,y,如果|x-y|=d,就会在这两个值之间连边,然后看d的正负从小到大或从大到小扫一遍。
但是注意,d和-d的区别只是所有值都从去小的那一边还是都去大的那一边。
也就是说,选择d和-d得到的是两个平移后相等的集合,那么下一步的选择d’的绝对值是不会改变的。

于是就可以把所有数的绝对值拿出来,排序后从大到小贪心。合法的情况下做出来的值里一定会包含0。先假设当前数是负的,然后从小到大扫一遍,判合不合法就好了。