高維前綴和/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;
}