[codeforces 618 F] Double Knapsack (抽屉原理)

题目链接:http://codeforces.com/contest/618/problem/F

题目:

题目大意:

有两个大小为 N 的可重集 A, B, 每个元素都在 1 到 N 之间. 分别找出 A 和 B 的一个子集, 使得这两个子集元素之和相等.

分别输出集合A和集合B的子集的个数以及子集中元素在原集合中的位置

N ≤ $10^6$

题解:

首先我们证明一个结论,存在一组方案,满足两个子集在A中和在B中都是连续的一段

把两个集合看成两个数组,分别计算出前缀和sa,sb

对于每个i(0<=i<=n),我们j为满足0<=sa[i]-sb[j]<=n-1的最大的j,设d[i]=sa[i]-sb[j],可以发现j的单调递增的

我们发现d数组一共有n+1个元素(包括i=0,sa[i]=0的情况),并且我们又发现d[i]的取值只有n个。那么根据抽屉原理,至少有两个d数组中的元素是相等的

于是我们有sa[i']-sb[j']=sa[i]-sb[j](i'>i)

移项之后得到sa[i']-sa[i]=sb[j']-sb[j]

这个时候我们就知道在A数组中i+1~i'这一段元素之和与B数组中j+1~j'这一段元素之和相等

证毕

其实上面的证明过程就是我们本题的做法,我们用two pointers处理出d数组,然后判断当前的d值在之前是否出现过,这个时候我们就得到了答案

值得注意的是,我们需要sa[n]<=sb[n],因为若是sa[n]>sb[n]可能出现对于i=n找不到一个j满足上述条件

代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;

const int N=1e6+15;
int n,cnt1,cnt2,st1,st2,ed1,ed2;
int sa[N],sb[N],vis[N],p[N];
inline int read()
{
    char ch=getchar();
    int s=0,f=1;
    while (ch<'0'||ch>'9') {if (ch=='-') f=-1;ch=getchar();}
    while (ch>='0'&&ch<='9') {s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*f;
}
void solve(int a[],int b[])
{
    memset(vis,-1,sizeof(vis));
    int j=0;
    for (int i=0;i<=n;i++)
    {
        while (a[i]-b[j]>=n) j++;
        int d=a[i]-b[j];
        if (vis[d]!=-1)
        {
            cnt1=i-vis[d];
            st1=vis[d]+1;ed1=i;
            cnt2=j-p[vis[d]];
            st2=p[vis[d]]+1;ed2=j;
            break;
        }
        vis[d]=i;p[i]=j;
    }
}
int main()
{
    n=read();
    for (int i=1;i<=n;i++) sa[i]=sa[i-1]+read();
    for (int i=1;i<=n;i++) sb[i]=sb[i-1]+read();
    if (sa[n]<=sb[n])
    {
        solve(sa,sb);
    }
    else 
    {
        solve(sb,sa);swap(cnt1,cnt2);swap(st1,st2);swap(ed1,ed2);    
    }
    printf("%d
",cnt1);
    for (int i=st1;i<=ed1;i++) printf("%d ",i);
    printf("
");
    printf("%d
",cnt2);
    for (int i=st2;i<=ed2;i++) printf("%d ",i);
    printf("
");
    return 0;
}
原文地址:https://www.cnblogs.com/xxzh/p/9535463.html