SOS dp
SOS dp是解决类似这样的问题
F
[
m
a
s
k
]
=
∑
i
⊆
m
a
s
k
A
[
i
]
\begin{aligned} F[mask] &= \sum _ {i \subseteq mask} A[i]\end{aligned}
F[mask]=i⊆mask∑A[i]
的一种解法(其实就是高维前缀和)。
这种问题,有不同的解法,比如:
1.时间复杂度 O ( 4 n ) O(4^n) O(4n)
for(int mask = 0;mask < (1<<N); ++mask){
for(int i = 0;i < (1<<N); ++i){
if((mask&i) == i){
F[mask] += A[i];
}
}
}
不过时间复杂度太高,正常一点的出题人出的题目一定过不了。
2.时间复杂度 O ( n 3 ) O(n^3) O(n3)
// iterate over all the masks
for (int mask = 0; mask < (1<<n); mask++){
F[mask] = A[0];
// iterate over all the subsets of the mask
for(int i = mask; i > 0; i = (i-1) & mask){
F[mask] += A[i];
}
}
时间复杂度证明见codeforces。
3.SOS dp
我们记 F [ m a s k ] [ i ] F[mask][i] F[mask][i] 为对于数 m a s k mask mask 的二进制中,第 i i i 位是不同的.
若 m a s k mask mask 的第 i i i 位是 0 0 0,则
F [ m a s k ] [ i ] = F [ m a s k ] [ i − 1 ] F[mask][i]=F[mask][i-1] F[mask][i]=F[mask][i−1]
若 m a s k mask mask 的第 i i i 位是 1 1 1,则
F
[
m
a
s
k
]
[
i
]
=
F
[
m
a
s
k
]
[
i
−
1
]
+
F
[
m
a
s
k
⨁
2
i
]
[
i
−
1
]
F[mask][i]=F[mask][i-1]+F[mask \bigoplus 2^i][i-1]
F[mask][i]=F[mask][i−1]+F[mask⨁2i][i−1]
简单来讲,就是:
上图描述了我们如何将
S
(
m
a
s
k
,
i
)
S(mask,i)
S(mask,i) 集相互关联。任何集合
S
(
m
a
s
k
,
i
)
S(mask,i)
S(mask,i)的元素都是其子树中的叶子。红色前缀表示掩码的这一部分对其所有成员/子级都是通用的,而掩码的黑色部分可以不同。
请注意,这些关系形成了一个有向无环图,而不一定是有根树。
在实现了这些关系之后,我们可以很容易地提出相应的动态规划。
代码:
//iterative version
for(int mask = 0; mask < (1<<N); ++mask){
dp[mask][-1] = A[mask]; //handle base case separately (leaf states)
for(int i = 0;i < N; ++i){
if(mask & (1<<i))
dp[mask][i] = dp[mask][i-1] + dp[mask^(1<<i)][i-1];
else
dp[mask][i] = dp[mask][i-1];
}
F[mask] = dp[mask][N-1];
}
还可以继续优化:
//memory optimized, super easy to code.
for(int i = 0; i<(1<<N); ++i)
F[i] = A[i];
for(int i = 0;i < N; ++i)
for(int mask = 0; mask < (1<<N); ++mask){
if(mask & (1<<i))
F[mask] += F[mask^(1<<i)];
}
注意,代码中的 N N N 为最大的二进制长度,即 N N N 为题目中给定的最大的 l o g n log n logn。
例题:
作为 CF 中 div2 的最后一题,本人认为有点水。
这其实就是一道高维前缀和的模板题。
分析:对于每一对 a i & a j = 0 ai \& aj = 0 ai&aj=0,若 a i ai ai 和 a j aj aj 的二进制表示中有同一位均为 1 1 1,那么 a i & a j ai \& aj ai&aj 的这一位也会为 1 1 1。因此可以发现, a i ai ai 是 a j aj aj 按位取反后的子集。
实现:预处理出数组 a a a 的高维前缀和,每一次询问时先取反,再看取反后子集是否为空。高维前缀和只需存储子集中的任意一个数即可。
我的代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll mod = 1e9 + 7;
const int N = 1000005;
const int INF = 0x3f3f3f3f;
int a[N];
int f[1 << 22];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
f[a[i]] = i;
}
for (int i = 0; i < 22; i++) {
for (int j = 0; j < 1 << 22; j++) {
if (f[j] == 0 && (j >> i & 1)) f[j] = f[j ^ 1 << i];
}
}
a[0] = -1;
for (int i = 1; i <= n; i++) {
printf("%d ", a[f[a[i] ^ (1 << 22) - 1]]);
}
printf("\n");
return 0;
}
若设
f
s
fs
fs 表示子集含有
s
s
s 的方案数,那么实际上
f
s
fs
fs 就是所有含
s
s
s 的集合的高维前缀和,倒着处理一遍所有含
s
s
s 的
a
a
a 的个数,那么显然
f
s
=
2
f
s
−
1
fs=2^{fs}-1
fs=2fs−1,接下来根据差分的思想再差分回去就好了。
代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll mod = 1e9 + 7;
const int N = 1000005;
const int INF = 0x3f3f3f3f;
int a[1 << 20];
ll two[N], f[1 << 20];
int main() {
int n;
scanf("%d", &n);
for (int i = 1, x; i <= n; i++) {
scanf("%d", &x);
a[x]++;
}
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 1 << 20; j++) {
if (!(j >> i & 1)) a[j] += a[j ^ 1 << i];
}
}
two[0] = 1;
for (int i = 1; i <= n; i++) two[i] = two[i - 1] * 2 % mod;
for (int j = 0; j < 1 << 20; j++) f[j] = two[a[j]] - 1;
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 1 << 20; j++) {
if (!(j >> i & 1)) f[j] -= f[j ^ 1 << i], f[j] %= mod;
}
}
printf("%lld\n", (f[0] + mod) % mod);
return 0;
}
最后,我发现这场 CF 做 D 题的人比做 A 的还多