容斥
容斥不仅有着各式各样的式子,还有正难则反,容斥的思想等重要的方法手段,是计数中非常核心的技术。
朴素容斥
朴素容斥就是最基本的在小学就学过的集合的容斥。
容斥原理
设每个元素有 \(n\) 个可能的属性 \(p_i\) 表示其属于第 \(i\) 个集合 \(S_i\),那么对于所有集合的并集,我们有
也就是说我们赋予集合一个顺序之后,如果我们知道所有 \(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\) 个中。所以我们有
凑二项式定理可以化简,得到
每个元素都只统计了一次,所以最后的总贡献就是并集的元素个数。
应用
Luogu P3214 卡农
在集合 \(S=\{1,2,\cdots,n\}\) 中选择 \(m\) 个无序互异非空子集,使得每个元素被选择的次数都为偶数,求方案数。
直接考虑无序有些困难,我们先考虑有序且符合其它限制的方案数。
注意到奇偶性的限制非常弱,实际上只用拿一个子集出来,根据其他元素被选择次数的奇偶性来调整这个子集里的元素。这个子集是唯一确定的。其它非空互异的子集在 \(2^n-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_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;
}