高维前缀和学习笔记 / CF449D Jzzhu and Numbers 题解

高维前缀和

\(~~~~\) 高维前缀和是用于解决有关子集或超集和问题的一种算法(或者说技巧?)

\(~~~~\) 形式化地,即求:

\[\sum_{i=0}^{2^n-1}\sum_{j \subset i} a_j \]

\(~~~~\)

\[\sum_{i=0}^{2^n-1}\sum_{j \supset i} a_j \]

\(~~~~\) 即每一个集合的子集或超集和,这里把一个数理解为状压的集合方案。

\(~~~~\) 一般来说,这样暴力做的时间复杂度为 \(\mathcal{O(3^n)}\) (枚举子集及其子集),但高维前缀和可以做到 \(\mathcal{O(n\times 2^n)}\)

引入

\(~~~~\) 我们回忆一下一般怎么求二维前缀和。

for(int i=1;i<=n;i++)
{
	for(int j=1;j<=m;j++) f[i][j]=f[i-1][j]+f[i][j-1]-f[i-1][j-1]+a[i][j];
}

\(~~~~\) 这是用容斥来做的,但我们还可以依次对行和列进行前缀和,即:

for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) f[i][j]=f[i-1][j]+a[i][j];
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) f[i][j]=f[i][j-1]+a[i][j];

\(~~~~\) 看起来好像带常数,但这是可以仅通过加入若干层枚举轻松拓展到更高维的,而容斥每加一维都并不好推。

\(~~~~\) 因此,不妨以这个思路,将集合的元素个数用维来代替,那就每次处理某一维的前缀和即可。

高维前缀和

子集

\(~~~~\) 求子集的和,那就是求高维前缀和。从低到高枚举元素(即维数),然后枚举所有 \(\in [0,2^n-1]\) 的元素,用上一层的来更新当前即可。

for(int j=0;j<n;j++)
{
	for(int i=0;i<(1<<n);i++) if((i>>j)&1) f[i]+=f[i^(1<<j)]; //有当前元素的才更新
}

超集

\(~~~~\) 求超集的和,那就是求高维后缀和。从高到低枚举元素(即维数),然后倒序枚举所有 \(\in [0,2^n-1]\) 的元素,用上一层的来更新当前即可。(为什么感觉跟上面这么像)

for(int j=0;j<n;j++)
{
	for(int i=(1<<n)-1;~i;i--) if(!((i>>j)&1)) f[i]+=f[i^(1<<j)];//没有当前元素才更新
}

\(~~~~\) (代码其实跟上面也很像)

\(~~~~\) 所以本质上这就是个状压,不过个人理解就直接把它定位为求子集或超集和的技巧。

例题 CF449D Jzzhu and Numbers

\(~~~~\) 其实我就是想写个题解。

题意

给出一个长度为 \(n\) 的序列 \(a_1,a_2...a_n\)。求构造出一个序列 \(i_1 < i_2 < ... < i_k(1\le{k}\le{n})\) 使得 \(a_{i_1} \text{and}\ a_{i_2} \text{and}\ \dots \text{and}\ a_{i_k}=0\) 。求方案数模 \(10^9+7\)

也就是从 \(\{a_i\}\) 里面选出一个非空子集使这些数按位与起来为 \(0\).

题解

\(~~~~\) 一眼发现正着不好算,考虑反过来去掉不合法的。

\(~~~~\) 再发现恰好有 \(k\) 位为 \(1\) 的也不好算,因此计算至少有 \(k\) 位为 \(1\) 的,出现至少的话显然就是容斥了。

\(~~~~\) 至少有 \(k\) 位为 \(1\) ,那我们需要计算出某个方案 \(i\) 的超集有多少个元素,即有多少出现过的数是 \(i\) 的超集,这样在选择 \(i\) 时选择这些数必定会使与和的 \(1\) 的个数至少为 \(\operatorname{Popcount(i)}\) 。统计超集直接上高维前缀和。

\(~~~~\) 最后,考虑某个 \(i\) ,若共有 \(p\) 个元素可选,显然就有 \(2^p-1\) 种选法至少有 \(\operatorname{Popcount(i)}\)\(1\) (即选超集的非空子集),对于每种 \(\operatorname{Popcount(i)}\) 容斥即可。

代码

查看代码
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
const ll MOD=1e9+7;
ll f[2000005],g[2000005];
ll qpow(ll a,ll b)
{
	ll ret=1;
	while(b)
	{
		if(b&1) ret=ret*a%MOD;
		b>>=1;a=a*a%MOD;
	}
	return ret;
}
ll Mul(ll a,ll b){return a*b%MOD;}
ll Add(ll a,ll b){return ((a+b)%MOD+MOD)%MOD;}
ll PopCount(ll x){ll ret=0;while(x){ret+=x&1;x>>=1;}return ret;}
int main() {
	int n;scanf("%d",&n);
	for(int i=1,x;i<=n;i++) scanf("%d",&x),f[x]++;
	for(int j=0;j<20;j++)
	{
		for(int i=(1<<20)-1;~i;i--) if(!((i>>j)&1)) f[i]+=f[i|(1<<j)];
	}
	for(int i=0;i<(1<<20);i++) g[i]=Add(qpow(2,f[i]),-1);
	ll Ans=0;
	for(int i=0;i<(1<<20);i++) Ans=Add(Ans,Mul(g[i],PopCount(i)&1?-1:1));
	printf("%lld",Ans);
	return 0;
}
原文地址:https://www.cnblogs.com/Azazel/p/14584435.html