数学 Trick 之:sos dp+反演
能够解决的问题
给定一些二进制数,求他们的与/或等于一个给定数的方法数。
优缺点
通用!通用!通用!
思路
不妨把这些二进制数看成 01 集,求他们的并/交等于一个给定集合的方法数。
为了下面的部分,我们定义给定的集合为 \(s_1, s_2, \dots, s_n\),答案的集合集交(并其实同理,只需要取反就行了)在一起要为 \(T\)。
我们如果要直接求答案的话要满足两个条件,对于任意满足条件的集合集 \(S_{now}\):
- \(T \subseteq S_{now}\)
- 对于一个 \(T\) 中一位的 \(0\),都存在一个 \(S_{now}\) 中的集合使得他的那一位为 \(0\)(否则他们的交的这一位就是 \(1\))。
第一的条件非常的 \(\text{beautiful}\),所以我们考虑不管第二个,而这个可以通过子集反演轻松反演回去,所以我们只需要看看第一个限制怎么办。
(因为要反演,所有所有的集合都要把它作为 \(T\) 求一次答案。)
令 \(\text{f}_S\) 为目标集合为 \(S\) 的只考虑限制 1 的答案。
我们发现这个可以直接 \(\text{sos}\;\text{dp}\) 秒掉,于是反演回去即可。
例题与代码
P6442 [COCI 2011/2012 #6] KOŠARE
将或为全集变为取反后交集为空,于是本 \(\text{Trick}\) 的板子。
#include <bits/stdc++.h>
using namespace std;
#define int long long
constexpr int maxn = 1000010, maxm = 25, maxS = 2000010, modd = 1e9 + 7;
int n, m, dp[maxS], up, ans, sz[maxS], sum[maxS];
int ksm(int a, int b) { // 快速幂
int ress = 1;
while (b) {
if (b & 1) ress = ress * a % modd;
a = a * a % modd;
b >>= 1;
}
return ress;
}
signed main() {
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
cin >> n >> m;
up = (1 << m);
for (int S = 0; S < up; S++) { // 集合大小
sz[S] = sz[S >> 1] + (S & 1);
}
for (int S = 0; S < up; S++) { // 反演系数
if (sz[S] & 1) sum[S] = -1;
else sum[S] = 1;
}
for (int i = 1, k, x, now; i <= n; i++) { // 输入&取反
cin >> k;
now = ((1 << m) - 1);
for (int j = 1; j <= k; j++) {
cin >> x;
now ^= ((1 << (x - 1)));
}
dp[now] += 1;
}
for (int i = 1; i <= m; i++) { // sos dp 顺便反演
for (int S = up; S >= 0; S--) {
if (!(S & (1 << (i - 1)))) {
dp[S] += dp[S ^ (1 << (i - 1))];
dp[S] %= modd;
}
}
}
for (int S = 0; S < up; S++) { // 统计答案(只是本题需要,算法流程不一定有这一步)
ans = (ans + sum[S] * (ksm(2, dp[S]) - 1) % modd) % modd;
}
cout << (ans % modd + modd) % modd;
return 0;
}

浙公网安备 33010602011771号