容斥基础

容斥

容斥不仅有着各式各样的式子,还有正难则反,容斥的思想等重要的方法手段,是计数中非常核心的技术。

朴素容斥

朴素容斥就是最基本的在小学就学过的集合的容斥。

容斥原理

设每个元素有 \(n\) 个可能的属性 \(p_i\) 表示其属于第 \(i\) 个集合 \(S_i\),那么对于所有集合的并集,我们有

\[\left| \bigcup_{i=1}^n S_i\right|= \sum_{x=1}^n(-1)^{x-1}\sum_{i_1<i_2<\cdots<i_x}\left|\bigcap_{j=1}^xS_{i_j} \right| \]

也就是说我们赋予集合一个顺序之后,如果我们知道所有 \(1\) 个、\(2\) 个、\(\cdots\)\(n\) 个元素的集合的交集大小,我们就可以求出这 \(n\) 个集合的并集大小。这就是朴素的容斥原理的式子。

注意这里的 \((-1)^{x-1}\) 也被称为容斥系数,也就是在容斥中让你要求的东西的系数为 \(1\) 或者一个常数的一个函数。下面我们将证明 \((-1)^{x-1}\) 是让每个元素统计贡献为 \(1\) 的容斥系数。

证明

我们考虑某个元素出现在 \(m\) 个集合 \(T_1,T2,\cdots,T_m\) 中,则这个元素被统计的次数 \(cnt\) 我们就可以用上式算出。
具体地,在每次拿 \(x\) 个集合来求并集时,由于集合被赋予了顺序,所以统计到的贡献就为 \((-1)^{x-1}{m\choose x}\),即选到的 \(x\) 个集合都在这 \(m\) 个中。所以我们有

\[cnt=\sum_{x=1}^m{(-1)^{x-1}}{m\choose x} \]

凑二项式定理可以化简,得到

\[\begin{aligned} cnt&=\sum_{x=0}^m(-1)^{x-1}{m\choose x}+1\\ &=(-1)^{m+1}\sum_{x=0}^m(-1)^x{m\choose x}+1\\ &=(-1)^{m+1}(1-1)^m+1\\ &=0+1=1 \end{aligned} \]

每个元素都只统计了一次,所以最后的总贡献就是并集的元素个数。

应用

Luogu P3214 卡农

在集合 \(S=\{1,2,\cdots,n\}\) 中选择 \(m\) 个无序互异非空子集,使得每个元素被选择的次数都为偶数,求方案数。

直接考虑无序有些困难,我们先考虑有序且符合其它限制的方案数。
注意到奇偶性的限制非常弱,实际上只用拿一个子集出来,根据其他元素被选择次数的奇偶性来调整这个子集里的元素。这个子集是唯一确定的。其它非空互异的子集在 \(2^n-1\) 个非空子集中选择,有

\[(2^n-1)^{\underline{i-1}} \]

\(f_i\) 表示选择 \(i\) 个互异非空子集且每个元素被选择的次数都为偶数的方案数,答案即 \(f_m\)。容易得到 \(f_1=f_2=0\),接下来我们考虑 DP 出 \(f_i\)

在上式的基础上,我们要考虑用以调整奇偶性的子集是否合法:如果前 \(i-1\) 个子集每个元素出现次数已经是偶数,这个子集为空不合法,那么就要减去这不合法的方案数 \(f_{i-1}\);如果这个子集与之前某个子集相同,那除开这 \(2\) 个集合,剩下 \(i-2\) 个子集构成了合法方案数即 \(f_{i-2}\),先前的子集都有可能与其相同,就有 \(i-1\) 种情况,但是这两个子集与其他子集都不相同,可能的集合就有 \(2^n-1-(i-2)\) 种。这两部分非法方案都减去就得到了合法的 \(f_i\),有

\[f_i=(2^n-1)^{\underline{i-1}}-f_{i-1}-f_{i-2}\times(i-1)\times(2^n-1-(i-2)) \]

递推求出 \(f_m\),求个逆元除以 \(m!\) 转成无序的即可。

代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 1e6 + 10, mo = 1e8 + 7;
int n, m, f[maxn];
ll fall_fac[maxn];

ll qpow(ll x, ll y) {
	ll res = 1;
	while(y) {
		if(y & 1) (res *= x) %= mo;
		(x *= x) %= mo, y >>= 1;
	}return res; 
}

int main() {
	ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
	
	cin >> n >> m; ll pow2 = qpow(2, n), fac = 1; f[0] = 1, fall_fac[1] = pow2 - 1;
	
	for(int i = 2; i <= m; i++) fall_fac[i] = fall_fac[i - 1] * (pow2 - i) % mo;
	for(int i = 2; i <= m; i++) { 
		f[i] = (fall_fac[i - 1] - f[i - 1] - (i - 1) * (pow2 - 1 - (i - 2)) % mo * f[i - 2] % mo) % mo; (f[i] += mo) %= mo;
	}
	for(int i = 2; i <= m; i++) (fac *= i) %= mo;
	
	cout << 1ll * f[m] * qpow(fac, mo - 2) % mo << endl;
	return 0;
} 
posted @ 2025-03-20 20:43  Ydoc770  阅读(24)  评论(0)    收藏  举报