高维前缀和/SOS dp
高维前缀和/SOS dp
概念
一般我们写的前缀和实际上是容斥的思想。
如:
for(int i=1;i<=n;++i)
S[i]=S[i-1]+A[i];
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+A[i][j];
设 \(t\) 为维度,\(n\) 为每个维度的最大值。那么这种容斥的写法的复杂度实际上是 \(O(n^t\times 2^t)\)。
而实际上我们还有另一种写法,也是高维前缀和统计所用的方法。
如:
for(int i=1;i<=n;++i)
S[i]=S[i-1]+A[i];
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
S[i][j]=S[i][j-1]+A[i][j];
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
S[i][j]=S[i-1][j]+S[i][j];
这种写法的思想其实就是一维一维的统计,这样的统计方法,复杂度就降到了 \(O(n^t\times t)\)。
而实际上,在高维的时候,\(n\) 大多为 \(2\) 。所以这个效率是十分高效的。举个例子。
对于 \(\forall i,i\in[1,n]\) 都有一个权值 \(A_i\),对于每一个 \(i\) ,请你输出 \(\sum A_j(j\& i==j)\)。\(n\le 10^6\)
一种暴力的想法,枚举每一个 \(i\) 与他的子集,暴力统计一波。时间复杂度是 \(O(3^{\log n})\) 。
实际上我们可以转化一下模型。对于一个数 \(i\) 我们将其转化为二进制,以 \(0110101\) 为例。我们发现,我们可以将这个数字看成一个 \(7\) 维的坐标,然后该数的权值就是这个坐标的权值,那么对于 \(0110101\) 来说,我们要统计的就是相当于所有坐标中满足每一维度的坐标都小于等于 \(0110101\) 每一维度的坐标的权值和。那么这就是个 \(7\) 维偏序,我们可以用高维前缀和解决。由于每一位上的最高值只有 \(2\) 那么总时间复杂度就是 \(O(2^{\log n}\times \log n)\),也就是 \(O(n\log n)\)。
而高维前缀和解决这类子集问题的方法,也经常被叫做 SOS dp。也就是说 SOS dp 的本质是高维前缀和。
代码如下:
for(int i=0;i<20;++i)//枚举当前处理哪一维度
for(int j=0;j<n;++i)
if((1<<i)&j) S[j]+=(S[j^(1<<i)]);//如果该维度为1,统计上该维度为0的前缀和
例题
CF449D Jzzhu and Numbers
给出一个长度为 \(n\) 的序列 \(a_1,\dots,a_n\),构造出一个序列 \(i_1\le i_2\le\dots \le i_k(1\le k\le n)\) 使得 \(a_{i_1}\&\dots\&a_{i_k}=0\)。求方案数。由于方案数可能很大,请你对 \(10^9+7\) 取模。(\(1\le n,a_i\le 10^6\))
设 \(A_i\) 表示 \(i\) 在序列中出现的次数,\(t_i\) 表示构造出一个序列使他们与和恰好为 \(i\) 的方案数,\(g_i\) 表示构造出一个序列,它们的与和的子集包含 \(i\) 的方案数。那么实际上 \(g_i\) 就是 \(t_i\) 的高维前缀和,如果我们能求出 \(g_i\),那么进行一遍差分就可以得到 \(t_i\) 了。
考虑如何得到 \(g_i\)。由于与和的子集包含 \(i\)。所以序列中的每一个数的子集肯定也包含 \(i\)。我们设 \(f_i=\sum{A_j}(i\&j==i)\),那么 \(g_i=2^{f_i}-1\)。
\(f_i\) 实际上就是求 \(i\) 的超集,我们将 \(i\) 取反后求一遍高位前缀和就能得到 \(f_i\) 了。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = (1<<20)+5;
const int MX = (1<<20);
const int MOD = 1e9+7;
int n,A[MAXN];
ll f[MAXN],g[MAXN];
ll qpw(ll x,ll b)
{
ll ans=1,tmp=x,now=1;
while(now<=b)
{
if(now&b) ans=ans*tmp%MOD;
tmp=tmp*tmp%MOD;
now<<=1;
}
return ans;
}
void Add(ll &x,ll y) {x=(x+y<MOD?x+y:x+y-MOD);}
void Del(ll &x,ll y) {x=(x-y<0?x-y+MOD:x-y);}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&A[i]),Add(f[A[i]],1);
for(int i=0;i<20;++i)
for(int j=0;j<MX;++j) if(!((1<<i)&j)) Add(f[j],f[j^(1<<i)]);//取反求超集
for(int i=0;i<MX;++i) g[i]=qpw(2,f[i]),Del(g[i],1);
for(int i=19;i>=0;--i)
for(int j=0;j<MX;++j) if(!((1<<i)&j)) Del(g[j],g[j^(1<<i)]);//差分求t[i]
printf("%lld\n",g[0]);
return 0;
}
COCI 2011-2012#6 KOSARE
在一个废弃的阁楼里放置有 \(n\) 个箱子,这些箱子里存放着 \(m\) 种玩具。对于第 \(i\) 个箱子,它里面有 \(k_i\) 个玩具(不同的箱子里可能有相同的玩具)。
现在你需要选出一部分箱子,使得它们中共有 \(m\) 种玩具(即所有种类的玩具都包含)。求选择的方案总数(\(\mod10^9+7\))。(\(1\le n,10^6,1\le m\le 20,0\le k_i\le m\))
由于 \(m\) 很小,所以我们可以状压。相当于我们要求一部分箱子使他们状态相与等于 \(2^m-1\)。那么我们设 \(A_i\) 表示 \(i\) 这个状态出现的次数,\(f_i=\sum{A_j}(i\&j=j)\),\(g_i=2^{f_i}-1\)。那么 \(g_i\) 就是或和是 \(i\) 的子集的方案数。设 \(t_i\) 为或和恰好为 \(i\) 的方案数,那么 \(g_i\) 就是 \(t_i\) 的一个高维前缀和,我们差分回去就能得到 \(t_i\)。当然也可以用容斥。
代码如下:(容斥)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MOD = 1e9+7;
const int MAXN = (1<<20)+5;
const int MX = (1<<20);
int n,m,A[MAXN];
ll f[MX];
ll qpw(ll x,ll b)
{
ll ans=1,tmp=x,now=1;
while(now<=b)
{
if(now&b) ans=ans*tmp%MOD;
tmp=tmp*tmp%MOD;
now<<=1;
}
return ans;
}
void Add(ll &x,ll y) {x=x+y<MOD?x+y:x+y-MOD;}
void Del(ll &x,ll y) {x=x-y<0?x-y+MOD:x-y;}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;++i)
{
int k,sta=0;scanf("%d",&k);
for(int j=1;j<=k;++j)
{
int x;scanf("%d",&x);
sta|=(1<<(x-1));
}
Add(f[sta],1);
}
for(int i=0;i<20;++i)
for(int j=0;j<(1<<m);++j) if((1<<i)&j) Add(f[j],f[j^(1<<i)]);
for(int i=0;i<(1<<m);++i) f[i]=qpw(2,f[i]),Del(f[i],1);
ll ans=0;
for(int i=0;i<(1<<m);++i)
{
int cnt=__builtin_popcount(i);
if(cnt%2==m%2) Add(ans,f[i]);
else Del(ans,f[i]);
}
printf("%lld\n",ans);
return 0;
}
CF383E Vowels
给出 \(n\) 个长度为 \(3\) 的由 \(′a′\) ~\(′z′\) 组成的单词,一个单词是正确的当且仅当其包含至少一个元音字母。 这里的元音字母是 \(a\)~\(x\) 的一个子集。 对于所有元音字母集合,求这 \(n\) 个单词中正确单词的数量平方的异或和。
首先考虑一个单词什么时候是一个正确的单词。由于字母数很少,只有 \(24\) 个,我们考虑将每个单词状态压缩。那么当一个单词是正确的,就说明他的状态 \(sta\) 与上元音字母的集合状态不为零。那么就是 \(n\) 减去所有状态 \(sta\) 与上元音字母的集合状态为零的个数。后面那个就是求元音字母集合状态的补集的高维前缀和。然后按题意统计就好。
复杂度 \(O(24\times 2^{24})\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = (1<<24)+5;
const int MX = (1<<24);
bool Small;
int f[MAXN],n;
char s[5];
bool Sunny;
int main()
{
// cout<<1.0*(&Sunny-&Small)/1024/1024<<"MB"<<endl;
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
scanf("%s",s+1);
int sta=0;
for(int j=1;j<=3;++j)
if(s[j]<='x') sta|=(1<<(s[j]-'a'));
f[sta]++;
}
for(int i=0;i<24;++i)
for(int j=0;j<MX;++j) if(((1<<i)&j)) f[j]+=f[j^(1<<i)];
int ans=0;
for(int i=0;i<MX;++i)
ans^=((n-f[i])*(n-f[i]));
printf("%d\n",ans);
return 0;
}