先给一道题目:
- 给定 n n n( n ≤ 20 n \le 20 n≤20)个数,用 a 1 a_1 a1 到 a n a_n an 表示,对于所有 i ∈ 0 , 2 n − 1 i\in 0,2\^n-1 i∈0,2n−1 求 f i = ∑ j ∈ i a j f_i=\sum_{j\in i}^{} a_j fi=∑j∈iaj,其中 i i i 表示一个集合。
这就是 SOS DP 的常见形式。
SOS DP 本质上就是一个高维前缀和,我们先思考一下二维前缀和是怎么求的:
cpp
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) s[i][j]+=s[i-1][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) s[i][j]+=s[i][j-1];
这是二维前缀和的其中一种写法,我们先将二维的其中一维相加,再将另一维相加。
仿造二维前缀和,我们可以写出 SOS DP:
cpp
for(int i=0;i<n;i++)
for(int j=0;j<(1<<n);j++)
if(j&(1<<i)) f[j]+=f[j^(1<<i)];
例题
COCI 2011/2012 #6 KOŠARE
题意
给定 n n n 个数,用 a 1 a_1 a1 到 a n a_n an 表示,选出若干个数使得这些数的或和为 2 m − 1 2^m-1 2m−1,问有多少种选数的方式。
n ≤ 10 6 , m ≤ 20 n\le 10^6,m\le 20 n≤106,m≤20。
思路
设 g i g_i gi 表示满足 a j ∈ i a_j\in i aj∈i 的 j j j 的个数, d p i dp_i dpi 表示有多少种选择方案使得选出的数的或和是 i i i 的子集 ,显然有 d p i = 2 f i − 1 dp_i = 2^{f_i}-1 dpi=2fi−1。
考虑容斥,答案即为 ∑ i = 0 2 m − 1 ( − 1 ) m − b i d p i \sum_{i=0}^{2^m-1} (-1)^{m-b_i} dp_i ∑i=02m−1(−1)m−bidpi,其中 b i b_i bi 表示 i i i 在二进制下 1 1 1 的个数。
有了 SOS DP,代码会非常好写。
代码
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5,mod=1e9+7;
int n,m,a[N];
long long g[N],dp[N],jie[N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int k;
cin>>k;
while(k--)
{
int x;
cin>>x;
a[i]+=(1<<(x-1));
}
g[a[i]]++;
}
for(int i=0;i<m;i++)
for(int j=0;j<(1<<m);j++)
if(j&(1<<i)) g[j]+=g[j^(1<<i)];
jie[0]=1;
for(int i=1;i<=n;i++) jie[i]=2ll*jie[i-1]%mod;
for(int i=0;i<(1<<m);i++) dp[i]=jie[g[i]]-1;
for(int i=0;i<m;i++)
for(int j=0;j<(1<<m);j++)
if(j&(1<<i)) dp[j]-=dp[j^(1<<i)];
cout<<(dp[(1<<m)-1]%mod+mod)%mod;
return 0;
}