子集反演 & 高维前缀和 & sos dp 学习笔记
子集反演 & 高维前缀和 & sos dp 学习笔记
子集反演
设 \(g(S)\) 表示集合 \(S\) 的答案,\(f(S)\) 为 \(S\) 的子集的答案和。
根据定义:
子集反演就是:
本质上就是容斥原理,可感性理解,证明略(给你你也记不住)。
于是便可以通过求 \(f\) 得到 \(g\)。
高维前缀和
从低维向高维考虑,先来看看二维前缀和:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
s[i][j]=s[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1];
这是运用了容斥原理,s[i-1][j]
和 s[i][j-1]
多加了一个 s[i-1][j-1]
,于是把它减掉。
但是我们可以运用更加普遍性的做法,对每一维分开求前缀和。
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
s[i][j]=s[i-1][j]+s[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
s[i][j]=s[i][j-1]+s[i][j];
我们先把行加起来,之后把加完的行按列加起来。
以第二种方法做三维前缀和也是类似的:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=1;k<=p;k++)
s[i][j][k]=s[i][j][k]+s[i-1][j][k];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=1;k<=p;k++)
s[i][j][k]=s[i][j][k]+s[i][j-1][k];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=1;k<=p;k++)
s[i][j][k]=s[i][j][k]+s[i][j][k-1];
更高维的前缀和也可以这样求。
于是我们得到了一个 \(O(n^tt)\) 的 \(t\) 维前缀和做法,先枚举维度,对每一维度分别求和。
例题
给定长为 \(n\) 的数组 \(a\),设 \(s_i=\sum_{j=1}^ia_j\),满足 \(s_n\le 10^{12}\)。
有 \(Q\) 个询问,每次给定 \(q\),满足 \(q|s_n\),求 \(\sum_{i=1}^n[q|s_i]\)。
\(n\le10^5,Q\le 10^5,1\le a_i \le 10^{12}\)
可知 \(q|s_n,q|s_i\),所以先把每个 \(s_i\) 对 \(s_n\) 取 gcd。
则此时 \(s_i\) 都为 \(s_n\) 的因数,考虑对 \(s_n\) 质因数分解,则
然后我们就能快速求出 \(s_j\) 的质因数分解:
则有 \(b_i\le c_i\)。
我们可以预处理每个 \(q\) 的答案。
我们把 \(s_n\) 的因数映射到一个较小的范围内。
设 \(f_i\) 为 \(i\) 是多少个 \(s_j\) 的因数,则初始 \(f_{s_j}=1\),我们求一遍高维后缀和,就能知道每一个因数的答案了。
如何映射?
把一个分解方式压成整数,可以把它看成每一位都是不同进制的数,第 \(i\) 位的进制即 \(c_i+1\)。
则第 \(i\) 为的基数为 \(w_i=\prod_{j=1}^{i-1}(c_j+1)\),此时一个因数的映射就是 \(p_i=\sum _{j=1}^k c_jw_j\)
转移数组
和上面高维前缀和一样,先枚举维度,固定其他维度,只变化这个维度做转移,大致代码如下:
for(int i=1;i<=k;i++)//维度
for(int j=S;j>=0;j--)//S为sn映射后的值
f[j]=f[j]+f[j+w[i]];
sos dp
对于每个 \(0\le i<2^n\),求 \(f_i=\sum _{j\in i} a_j\)。
朴素的做法是直接枚举子集,暴力地是 \(O(4^n)\),初始时 \(f_i=a_i\)。
for(int i=0;i<1<<n;i++)
for(int j=0;j<i;j++)
if((i|j)==i) f[i]+=f[j];
而众所周知,枚举子集可以做到 \(O(3^n)\),于是
for(int i=1;i<1<<n;i++){
f[i]+=f[0];
for(int j=i;j;j=(j-1)&i)
f[i]+=f[j];
}
然而,引入高维前缀和的思想,每一位都可以看做一个维度,先枚举维度,对每个维度分别求和,可以做到 \(O(n2^n)\)。
for(int j=0;j<n;j++)
for(int i=0;i<(1<<n);i++)
if(i&(1<<j))f[i]=f[i]+f[i^(1<<j)];
我们还可以对超集求和(如果 \(S\) 是 \(T\) 的子集,那么 \(T\) 是 \(S\) 的超集),只要把 0 变成 1 做即可,注意由于小的由大的转移过来,因此需要从大往小枚举:
for(int j=0;j<n;j++)
for(int i=(1<<n)-1;i>=0;i--)
if(!(i>>j&1))f[i]=f[i]+f[i^(1<<j)];
另外,对子集或超集求最值也是类似的。
例题 P6442 [COCI2011-2012#6] KOŠARE
题目大意:给定全集的 \(n\) 个子集,求使得所选集合的并集为全集的选择方案数,全集大小为 \(m\)。
- \(n\le 10^6,m\le 20\)。
由于 \(m\le 20\),我们把一个子集压成二进制。
设 \(a_S\) 为集合为 \(S\) 的个数。
我们设 \(b_S=\sum _{T\in S} a_T\),即集合为 \(S\) 的子集的个数。
那么设 \(f_S=2^{b_S} -1\),就是并集为 \(S\) 的子集的方案数。
\(b_S\) 可以用 sos dp 求。
知道 \(f_S\) 后,根据子集反演就可以得到答案为
二进制下的 \(|S|\) 就是 \(popcnt\),即二进制中 \(1\) 的个数。
例题 arc184_b
题目大意:选择 \(x\),可以覆盖 \(x,2x,3x\),求最少选多少个数可以覆盖完所有 \([1,n]\) 的整数,\(1\le n\le 10^9\)。
可以把所有数表示成 \(2^x3^yz\),其中 \(2\nmid z\and 3\nmid z\)。
然后可以把 \(z\) 相同的数放在一起,每个数放在第 \(x\) 行、第 \(y\) 列的位置,然后组成一个阶梯状的表。
那么题目相当于选择一个数,把它自己、它下面、和它右边的数覆盖。
由于 \(\log_3 n\le18\) 于是考虑状压 DP。
首先第一行要满足不能出现相邻的 0。
然后考虑后面的行。我们把上一行在这一行范围内的状态取反,相当于我们这一行选的 1 要覆盖这些取反后的 1。
如果我们这一行选了 \(S\),那么这一行能覆盖 S|(S<<1)
。能覆盖的状态就是 S|(S<<1)
的子集,做一遍 sos dp 即可。
但是每个数都要遍历一遍,复杂度是不小于 \(O(n)\) 的。
考虑优化,通过打表可以发现,对于 \(\Big \lfloor \dfrac n z\Big \rfloor\) 相同的 \(z\),它们的表长的一样。这些 \(z\) 批量处理即可。
时间复杂度不好表示,总之效率大大优化,可以在 \(2s\) 内通过。
类似的题 P3226 HNOI2012 集合选数、TFSETS - Triple-Free Sets。
本题代码:
int n;
int ans=0;
int l[31],d;
int f[31][1<<20],g[1<<20];
signed main(){
read(n);
for(int L=1,R;L<=n;L=R+1){
R=n/(n/L);
int cnt=0;
fo(i,L,R)if(i%2!=0&&i%3!=0)++cnt;
fo(i,L,R){
if(i%2!=0&&i%3!=0){
int x=i;
d=0;
while(x<=n){
l[++d]=0;
unsigned y=x;
while(y<=n)y*=3,++l[d];
x<<=1;
}
fu(j,0,1<<l[1]){
if(((j|(j<<1))&((1<<l[1])-1))==(1<<l[1])-1)f[1][j]=popcnt(j);
else f[1][j]=1e9;
}
fo(j,2,d){
fu(k,0,1<<l[j])g[k]=1e9;
fu(k,0,1<<l[j-1]){
gmin(g[((1<<l[j])-1)^(k&((1<<l[j])-1))],f[j-1][k]);
}
fu(p,0,l[j])fu(k,0,1<<l[j]){
if(k>>p&1)g[k]=min(g[k],g[k^(1<<p)]);
}
fu(k,0,1<<l[j]){
f[j][k]=g[(k|(k<<1))&((1<<l[j])-1)]+popcnt(k);
}
}
int sum=1e9;
fu(j,0,1<<l[d]){
sum=min(sum,f[d][j]);
}
ans+=cnt*sum;
break;
}
}
}
write(ans);
return 0;
}