【UOJ750】【UNR #6】小火车(抽屉原理+二分)
- 给定一个长度为 \(n\) 的序列 \(a\),要求构造一个长度为 \(n\) 的序列 \(b\) 满足 \(b_i\in\{-1,0,1\}\) 且 \(\sum_{i=1}^na_ib_i\equiv0(\operatorname{mod}p)\)。
- \(1\le n\le 40\),\(1\le p< 2^n\)
抽屉原理
对于这里 \(b_i\in\{-1,0,1\}\) 的限制,容易想到一个转化:选出两个不同的元素集,要求它们的和在模 \(p\) 意义下相等,则用每个 \(i\) 在第一个集合中的存在性减去它在第二个集合中的存在性即可得到 \(b_i\)。
而根据抽屉原理,总共 \(2^n\) 种元素和在模 \(p\) 意义下一定会有重复。由此证明了这道题一定有解,而且也得到了一个初步的解题思路。
二分寻找重复值
既然 \(2^n\) 种元素和在模 \(p\) 意义下一定会有重复,我们可以尝试去找出一个重复的值。
可以二分,初始值域范围为 \([0,p-1]\),然后每次将这个范围分为两半,显然至少有一半会满足其中的元素和个数大于这一半的值的种数,只要继续考虑这一半就必然能找出一个重复的值。
询问元素和在某一范围内的集合个数,可以 Meet in Middle 预处理后每次双指针求解。
代码:\(O(2^{\frac n2}n)\)
#include<bits/stdc++.h>
#define Cn const
#define CI Cn int&
#define N 40
#define LL long long
#define fi first
#define se second
using namespace std;
int n,c;LL p,a[N+5];pair<LL,LL> A[(2<<N/2)+5],B[(1<<N/2)+5];
int cA;void dfs1(int x,LL v,LL s)//Meet in Middle 前半部分
{
if(x>n/2) return (void)(A[++cA]=make_pair(v,s));dfs1(x+1,v,s),dfs1(x+1,(v+a[x])%p,s|(1LL<<x));
}
int cB;void dfs2(int x,LL v,LL s)//Meet in Middle 后半部分
{
if(x>n) return (void)(B[++cB]=make_pair(v,s));dfs2(x+1,v,s),dfs2(x+1,(v+a[x])%p,s|(1LL<<x));
}
bool Check(LL L,LL R)//检验[L,R]中元素和个数是否大于R-L+1
{
int i,l=1,r=0;LL t=0;for(i=cB;i;--i) {while(l<=cA&&A[l].fi+B[i].fi<L) ++l;while(r<cA&&A[r+1].fi+B[i].fi<=R) ++r;t+=r-l+1;}return t>R-L+1;//双指针
}
int fg,g[N+5];void M(LL s)//记录找到的一个集合s
{
int i;if(fg) {for(i=1;i<=n;++i) s>>i&1&&++g[i],printf("%d ",g[i]);exit(0);}for(i=1;i<=n;++i) s>>i&1&&--g[i];fg=1;
}
void Get(LL x)//找出两个元素和为s的集合
{
int i,j,l=1,r=0;for(i=cB;i;--i) {while(l<=cA&&A[l].fi+B[i].fi<x) ++l;while(r<cA&&A[r+1].fi+B[i].fi<=x) ++r;for(j=l;j<=r;++j) M(A[j].se|B[i].se);}
}
int main()
{
int i;for(scanf("%d%lld",&n,&p),i=1;i<=n;++i) scanf("%lld",a+i);
dfs1(1,0,0),c=0,dfs2(n/2+1,0,0),sort(A+1,A+cA+1),sort(B+1,B+cB+1);for(i=1;i<=cA;++i) A[cA+i]=A[i],A[i].first-=p;cA<<=1;
LL l=0,r=p,u;while(l^r) Check(l,u=l+r-1>>1)?r=u:l=u+1;return Get(l),0;//二分
}
待到再迷茫时回头望,所有脚印会发出光芒