集合并卷积
因为小星星那题才知道有这么个东西。。
下面这段从uoj复制的:http://liu-runda.blog.uoj.ac/blog/2360
题目
给出h[0…(2n)−1],满足h[x]=sigma{f[i]*g[j],1<=i<=n,1<=j<=n,i|j==x}
我们记F[i]=sigma(f[j],j&i==j),G[i]=sigma{g[j],j&i==j},H[i]=F[i]*G[i],那么H[i]=sigma{h[j],i&j==j}, 通过枚举子集的子集可以O(3^n)得到H[]数组,通过枚举子集的子集可以O(3^n)用H[]数组容斥出h[]数组,求 h[i]的时候,枚举i的所有子集,如果子集j的元素个数和i相差偶数个那么H[j]对h[i]的贡献是正的,如果子集j的元素和i相差奇数个那么H[j]对h[i]的贡献是负的.
也就是说: h[i]=sigma{H[j]*(-1)^(cnt1(i-j)),j&i==j},cnt1(x)表示x的二进制表示中1的个数
接下来我们把O(3^n)的容斥算法优化到O(n*2^n).
考虑第一步预处理,我们需要求出F[x]=sigma(f[i],i&x==i)(G数组的处理和F数组相同) 如果我们求出了1到x-1的F值,并不能利用前面这些项较快地计算出F[x]。
不妨考虑x的最高位2^i,我们把x的子集分成两部分,一部分包含元素i,一部分不包含元素i,不包含元素i的部分就是F[x-(2^i)],关键是包含i的部分.我们无法从F[1],F[2],…一直顺序推到F[2^n-1]
我们记F[j][i]为只允许最后j位和i不同的i的所有子集的和,那么边界是F[0][i]=f[i] F[j][i]=F[j-1][i](i的第j位为0) =F[j-1][i]+F[j-1][i-(2^j)] 前一维可以直接滚掉,那么我们就用O(n*2^n)的时间解决了F数组和G数组
(其实是一个n维前缀和?这个技巧是以前别人讲课的时候看见的) 接下来考虑H数组.我们发现从H数组求h数组相当于从F数组求f数组,我们把之前的DP过程倒过来就可以了…就是加号改成减号…因为每个二进制位本质一样所以从高位向低位枚举还是从低位向高位枚举都一样.
这个地方也可以理解为,h[j][i]为只允许最后j位和i不同的i的所有子集对i的贡献之和,
边界是 h[0][i]=f[i]
h[j][i]=h[j−1][i](i的第j位为0)
h[j][i]=h[j−1][i]−h[j−1][i−(2j)](i的第j位为1) 可以理解为每增加一个二进制位会使贡献的正负号改变一次所以把加号变成减号.
总体上感觉和斯坦纳树有点像加上了一点dp的技巧。。再加上一点容斥
代码:
#include <bits/stdc++.h> using namespace std; int n,m,lim,f[10000],g[10000],h[100000]; int main() { freopen("noip.in","r",stdin); freopen("noip.out","w",stdout); cin>>n; lim=1<<n; for (int i=1;i<=lim-1;i++) cin>>f[i]; for (int i=1;i<=lim-1;i++) cin>>g[i]; for (int i=0;i<n;i++) for (int j=0;j<=lim-1;j++) if ((j>>i)&1) f[j]+=f[j^(1<<i)]; for (int i=0;i<n;i++) for (int j=0;j<=lim-1;j++) if ((j>>i)&1) g[j]+=g[j^(1<<i)]; for (int i=0;i<lim;i++) h[i]=g[i]*f[i]; for (int i=0;i<n;i++) for (int j=0;j<=lim-1;j++) if ((j>>i)&1) h[j]-=h[j^(1<<i)]; for (int i=1;i<lim;i++) cout<<i<<" "<<h[i]<<endl; }