GYM100702 D

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

#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<cmath>
#include<iostream>
#include<algorithm>
#define N 11000
#define LL long long
using 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。先假设当前数是负的,然后从小到大扫一遍,判合不合法就好了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值