数学 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;
}
posted @ 2025-04-21 21:30  porse114514  阅读(12)  评论(0)    收藏  举报