容斥学习笔记
容斥原理
原理:
这东西学过小学奥数就会了。
一些有用的结论:
应用
基础应用
-
错排
问题: 求出大小为 \(n\) 的排列的错排数。
思路:
很明显可以用 \(D_n=(n-1)(D_{n-1}+D_{n-2})\) 来做,但是其实也可以容斥。
\(S_i\):\(P_i\not= i\) 的排列集合。
答案即为:\(|\bigcap_{i=1}^nS_i|\)。
根据结论:
而 \(S\) 的补集就是 \(P_i = i\) 的排列,相当于有 \(m\) 个位置确定了,所以:
这样就得到了 \(D_n\) 的通项公式,可以证明这个数趋近于 \(\frac{1}{e}\)。
-
第二类斯特林数
问题: 求第二类斯特林数 \(n,k\),即 \(n\) 个球放入 \(k\) 个盒子,盒子无顺序,盒子非空。
思路:
同样,也有递推公式,但是也可以容斥。
首先,假定盒子有顺序,最后除以 \(k!\) 即可。
\(S_i\):第 \(i\) 个盒子非空的方案。
答案即为:\(|\bigcap_{i=1}^nS_i|\)。
同理,转化成求:
现在考虑有 \(m\) 个盒子一定为空,则每个球都只有 \((k-m)\) 中选择,总共有 \((k-m)^n\) 种。
经过推导就可以得到:
又因为 \(|\Omega|=k^n\),所以答案即为:
-
欧拉函数
问题: 求欧拉函数 \(\varphi(n)\)。
思路:
设 \(n = \prod_{i=1}^kp_i\)。
\(S_i\):不被 \(p_i\) 整除的数。
答案即为:\(|\bigcap_{i=1}^nS_i|\)。
同理,转化成求:
现在要求的即是被 \(p_{a_1}p_{a_2}\dots p_{a_m}\) 整除的数的个数,即:\(\frac{n}{p_{a_1}p_{a_2}\dots p_{a_m}}\)。
所以可以变为:
这个其实就是 \(n\prod_{i=1}^k(1-\frac{1}{p_i})\) 的展开。
-
不定方程
问题: 求不定方程 \(\sum_{i=1}^nx_i=m\) 的非负解的个数,要求 \(x_i \le b_i\)。
思路:
首先,如果没有性质,那就是经典问题,挡板法可知答案为 \(\binom{n+m-1}{n}\)。
于是依然考虑容斥。
\(S_i\):\(x_i \le b_i\) 的方案。
答案即为:\(|\bigcap_{i=1}^nS_i|\)。
同理,转化成求:
现在就是要求 \(x_{a_i} \le b_{a_i}+1\)
而我们可以先给所有的 \(x_{a_i}\) 减去 \(b_{a_i}+1\),然后就成了没有限制的挡板法,可以直接计算。
然后就做完了。
-
互素数对
问题: \(n\) 个数 \(a_1 \sim a_n\),求数对 \((i,j)\) 个数,要求 \(a_i, a_j\) 互素。\(n \le 10^5, a_i \le 10^6\)。
思路:
容斥。
设 \(p_1 \sim p_k\) 为 \(10^6\) 以内的素数。
\(S_i\):所有都能整除 \(p_i\) 的数对。
答案即为:
考虑到都在 \(10^6\) 以内,预处理出每个 \(x\) 的倍数个数,暴力搜索即可。
题目
-
P1450 [HAOI2008] 硬币购物
思路:
这道题的重点在于如何转换为容斥原理,我们把每一种使得最终和为 \(s\) 的方案(没有数量限制)看作一个元素,则假设有所有方案的集合为 \(S\),而方案中第 \(i\) 种硬币数量超出 \(d_i\) 的所有方案的集合为 \(A_i\),则我们需要求的答案其实就是:
那么该如何的去求出 \(|S|\) 与 \(|\bigcup_{i=1}^4A_i|\) 呢?
首先我们设 \(dp_i\) 表示硬币数量不受限制,最终和为 \(i\) 的方法数,这很明显是一个完全背包,由于题目中 \(s \le 10^5\) ,所以直接预处理即可,这样 \(|S|\) 就是 \(dp_s\)。
void init() {
dp[0] = 1;
for (int i = 1; i <= 4; i++) {
for (int j = c[i]; j <= 100000; j++)
dp[j] += dp[j - c[i]];
}
}
考虑如何求 \(|A_i|\),我们可以先取 \(d_i+1\) 个 \(i\) 种硬币,那么还剩下 \(s-(d_i+1) \times c_i\) 的金额,再怎么取 \(i\) 的数量肯定超出限制,于是方法数即为 \(dp_{s-(d_i+1) \times c_i}\),然后就求出了 \(|A_i|\)
同理,如果 \(i\) 与 \(j\) 都超出了限制,你们方法数也应该为 \(dp_{s-(d_i + 1) \times c_i - (d_j+1) \times c_j}\) ,三个或四个的以此类推。
这很明显是一个容斥,于是直接根据容斥原理算,只用枚举每次是哪几类超出限制即可,复杂度 \(O(2^4n)=O(16n)=O(n)\)。
如果这题物品种类有 \(m\) 个,复杂度就是 \(O(n2^m)\)。
-
P5505 [JSOI2011]分特产
题目链接:P5505 [JSOI2011]分特产
思路:
这道题的重点以人为单位来计数。
首先说一下可重组合,即把 \(n\) 分成 \(m\) 个非负整数集合,它们的和为 \(n\) 的方法数,我们用小学奥数的挡板法即可得到答案是:\(C_{n+(m-1)}^{m-1}\)。
我们设 \(T_{i,k}\) 表示把第 \(i\) 种特产,数量为 \(a_i\),分给 \(k\) 个人的方法数(不一定每个人都要分到)。这个问题和上面其实是同一个问题,所以 \(T_{i,k}=C_{a_i+(k-1)}^{k-1}\)。
设 \(N_k\) 为把所有特产分给 \(k\) 个人的方法数(依然有人可能没拿到),因为每个特产都要被分发,且第 \(i\) 种特产分给 \(n\) 个人的方法数是 \(T_{i,n}\),所以这是一个乘法原理,即:
设集合 \(A_i\) 为第 \(i\) 名同学没有被分到特产的所有方案的集合,\(S\) 为所有人分所有特产(有人可以没分到)的方案的集合,因为我们要保证每个人都被分到,所以我们要求的就是:\(|S|-|\bigcup\limits_{i=1}^nA_i|\)。
很明显 \(|S| = N_n\),考虑如何求 \(|\bigcup\limits_{i=1}^nA_i|\)。我们先考虑如果某一个同学没拿到特产的方法数(别的也不一定都拿到)应该为 \(N_{n-1}\),因为有 \(n\) 个人,所以总共是: \(C_n^1 \times N_{n-1}\)。在考虑有两个人没拿到特产的方法数,总共应该是 \(C_n^2 \times N_{n-2}\),而上面这两者是有交集的,于是根据容斥原理,我们可以以此类推,得到:
所以答案其实就是:
然后就快乐的 AC 了~~
-
P6076 [JSOI2015]染色问题
题目链接:P6076 [JSOI2015]染色问题
思路:
设 \(T_i\) 为有 \(i\) 种颜色确定不用,剩下的颜色随意的方法数,则根据容斥原理,我们要求的就是:
考虑如何求 \(T_k\)。我们记 \(N_i\) 为有 \(i\) 行确定不涂色,其他行随意的,但是每一列都有颜色的方法数,则根据容斥原理,很明显:
考虑如何求 \(N_i\)(别烦,这时最后一个了)。我们考虑把每一列拆开来,因为每一列有 \(n-i\) 个格子需要染色,每个格子有 \(c-k+1\) 种染色方法(不染色也算一种),所以对于一列来说,共有 \((c-k+1)^{n-i}\) 种染色方式,但是不能全部不染色,所以还要减去一,即 \((c-k+1)^{n-i}-1\)。因为总共有 \(m\) 列,并且每一列是相互独立的,于是就知道了:
然后就可以求出 \(T_k\),最后求出答案了。
在具体实现中,其实并不需要去真的开三个数组。这题除了求组合数时阶乘以及逆元需要开个数组,其他都没必要。
代码很简单,但其实思维难度还是很高的。
-
AGC005D ~K Perm Counting
题意:
给定 \(n,k\),求所有满足 \(|p_i - i| \not = k\) 的 \(n\) 长度排列个数,\(n, k \le 2000\)。
思路:
我们将每个排列一一映射到一个 \(n \times n\) 棋盘,第 \(i\) 行第 \(p_i\) 个有棋子,显然每行每列恰好一个棋子。
我们将所有不能放的位置标出来,大致是两条斜向下的斜线。这条线上的都不能放。
考虑容斥,\(f_i\) 表示放了至少 \(i\) 个再不能放的位置山,答案即为 \(\sum_{i=0}^n (-1)^i f_i(n-i)!\)。
数据范围不大,可以考虑 \(dp\)。
我们发现我们可以将同一行或同一列不能放的连边,可以形成若干条链,每条链相邻两个不能选。将所有链拼在一起,升个维就可以 \(dp\) 求出 \(f\) 了。
-
ARC093F Dark House
题意:
\(2^n\) 个人打淘汰赛,共 \((2^n)!\) 中顺序,你是 \(1\) 号,有 \(M\) 个人你打不过,其他人都打得过。而除了你之外的人编号小的更强。求有多少中顺序满足你能获得第一。
\(n \le 16, m \le 16\)。
思路:
首先,由于比赛是对称的,不妨假定 \(1\) 号初始在第 \(1\) 个位置,最后乘上 \(n\) 即可。
我们可以发现,\(1\) 号需要战胜的是 \(n\) 组选手的最小值,也就是到根节点路径上每个节点的另一个子树的所有点。
我们考虑如何求得方案使得这 \(m\) 个人都不会在组里获胜。
我们考虑子集反演,设 \(f(S)\) 表示 \(S\) 中组都是打不过的人获胜,其其他组都不是。\(g(S)\) 表示 \(S\) 中组都是打不过的人获胜,其他组任意。显然会有:
反演得到:
答案就是 \(f(\empty)\)。
这样就只用求 \(g(T)\) 了。
考虑 dp,设 \(dp[i][S]\) 表示填了前 \(i\) 个打不过的,已经填了组的集合是 \(S\),有多少种方案。
这种类似匹配的经典技巧是将 \(a_1 \sim a_m\) 从大到小处理,然后可以快速计算每个人有多少个剩余填法。
这样 \(g(S)\) 就等于 \(dp[m][S] \times (2^n - 1 - tot(S))\),其中 \(tot(S)\) 表示 \(S\) 所占的位置总个数。
很好的子集反演和 dp 题。
考虑容斥,我们将 \(|P_i - i| < x\) 视作每个 \(i\) 的性质,然后我们算满足 \(k\) 个性质的排列有多少,最后容斥一下即可。
由于数据范围很小,显然是 DP,我们将 \(p_i\) 置换,即 \(q_{p_i} = i\),然后限制就是我们要求有 \(k\) 个数取值在区间 \([i - x + 1, i + x - 1]\),其它随便,不能重叠。由于区间长度很小,考虑状压。设 \(dp_{i,j,S}\) 表示前 \(i\) 个填了 \(j\) 个,用掉了 \([i - x + 1, i + x - 1]\) 中的位置集合为 \(S\) 的方案数。转移非常简单。
然后就做完了。
显然容斥,\(f(i)\) 表示至少 \(i\) 个元素出现次数小于等于 \(1\) 次,答案就是 \(\sum(-1)^kf(k)\)。
考虑如何计算 \(f(k)\),首先我们需要选出 \(k\) 个,这是 \(\binom{n}{k}\)。
然后我们考虑这 \(k\) 个,将这 \(k\) 个分成若干个非空集合,使得每个元素出现次数不超过 \(1\) 次。
不妨设 \(s_{i,j}\) 表示将 \(i\) 个有区别的东西放入 \(j\) 个无区别的盒子中,可以有些东西不放,盒子非空,求方案数。
这个看着很想第二类斯特林数,其转移方程与其类似:\(s_{i,j} = (j+1)s_{i - 1,j} + s_{i-1,j-1}\)。
于是我们枚举分了 \(m\) 组,这 \(k\) 个数的方案数就是 \(s_{k, m}\)。
再考虑剩下的,不包含这 \(k\) 个数的集合有 \(2^{n-k}\),我们都可以选或不选,有 \(2^{2^{n-k}}\) 种。
对于这 \(m\) 个集合,每个集合对于其他的 \(n-k\) 个也是可以选和不选,有 \((2^{n-k})^m\) 种。
所以我们就得出了 \(f(k)\) 的计算公式:
最终复杂度 \(O(n^2 \log n)\),不要忘了对指数需要模 \(M - 1\)。
经典结论:找重心,然后任意一对点不在同一棵子树中。
如果有两个重心,答案就是 \((\frac{n}{2}!)^2\)。
否则,我们把重心拎出来,将 \(i\) 是否与 \(p_i\) 属于同一儿子的子树作为条件来容斥。
设 \(f(i)\) 表示至少有 \(i\) 个属于同一儿子的子树,答案即为 \(\sum (-1)^kf(k)(n-k)!\)。
对于一个大小为 \(m\) 的子树,选出 \(j\) 个数所对应的数的方案数是 \(\binom{m}{j}^2j!\)。
所以我们可以用一个背包求出 \(f(i)\)。
这道题重点在于找到适合的条件容斥。
DP 中带容斥系数
在一类题中,我们需要用 dp 求答案,最后再容斥算出答案,这样复杂度与 dp 有关。
但是我们也可以将容斥系数直接套进 dp 里,这样可以减少一维状态。
题意: 一棵树,但是边有方向,求拓扑序方案数。
思路:
如果这棵树是内向树或外向树,显然我们可以用 dp 求解。
所以我们考虑计算外向树,然后把所有不符合条件的边容斥掉。不妨设 \(f_{i,j,k}\) 表示 \(i\) 的子树内 \(j\) 条边由不符合反转成符合,\(i\) 所在连通块大小为 \(k\)。
显然是 \(O(n^3)\),我们发现最后的答案是 \(\sum (-1)^jf_{rt, j, k}\),所以我们可以考虑将 \(j\) 这一维代入 dp 值里。
设 \(g_{i,k} = \sum (-1)^j f_{i,j,k}\),则我们考虑对 \(g\) 进行 dp。
我们可以先把 \(f\) 的转移写出来:
如果这条边符合:
如果不符合:
发现这就是类似卷积的形式,于是我们可以将 \(g\) 的转移写出来:
然后我们就做完了。
题意: 一棵树,求多少种方案将顶点两两配对,使得将每个点对的路径上的所有边全部系上丝带后满足,每条边都有至少一条丝带。
思路:
将 \(n\) 个点两两配对的方案数是所有小于 \(n\) 的奇数的乘积。这个很容易就可以推出来。
如果我们钦定哪些边不覆盖,方案数很好计算,所以我们考虑容斥,\(F(i,j,k)\) 表示 \(i\) 子树内,有 \(j\) 条边未覆盖,其他任意,当前与 \(i\) 联通的点有 \(k\) 个,求方案数。
接着就是和上道题几乎一样的容斥与转移。
约数容斥
约数容斥主要解决环计数相关问题。
约数容斥基于以下事实:
甚至不需要证明,只是将 \(f(i)\) 单独领出来而已。
计算这类问题,加上记忆化后时间复杂度是 \(O(n^{\frac{2}{3}})\)。
题意: 生成一个长度为 \(n\) 的序列,过程如下:先生成一个 \(n\) 长度且每个元素 \(1 \sim k\) 的回文串,然后进行若干次将第一个元素移到最后,求有多少种不同的序列。\(n \le 10^9\)
思路:
环计数需要考虑最小循环节。不妨设 \(f(i)\) 表示最小循环节长度为 \(i\) 的个数,\(g(i)\) 表示循环节为 \(g(i)\) 的个数。
观察题目性质,发现回文串的循环节一定是回文串。
所以我们可以得到 \(g(i) = k^{\lceil\frac{i}{2}\rceil}\),进而可以通过约数容斥计算出 \(f(i)\)。
考虑统计答案,\(i\) 为奇数时可以贡献 \(i \times f(i)\),否则贡献 \(\frac{i}{2} \times f(i)\)。
然后就做完了。一定记得需要用 map 实现记忆化。
-
[无题号]Aku no Hanabira
题意: 有 \(K\) 种颜色球,每种颜色 \(c_i\) 个,将这些球排成一个圆,求有多少本质不同的圆?(只考虑旋转,不考虑翻折)\(K \le 10^5, n = \sum_{c_i} \le 10^6\)。
思路:
依然是环计数,容易发现计算 \(g(i)\) 就是计算多重集的排列,可以在 \(O(Kn^{\frac{2}{3}})\) 解决。
点击查看代码
#include <iostream>
#include <vector>
#include <map>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6 + 5;
const int mod = 1e9 + 7;
int k, c[N] = {0};
int fpow(int a, int b, int p) {
if (b == 0)
return 1;
int ans = fpow(a, b / 2, p);
ans = 1ll * ans * ans % p;
if (b % 2 == 1)
ans = 1ll * a * ans % p;
return ans;
}
int fac[N] = {0}, inv[N] = {0};
void init(int n) {
fac[0] = 1;
for (int i = 1; i <= n; i++)
fac[i] = 1ll * fac[i - 1] * i % mod;
inv[n] = fpow(fac[n], mod - 2, mod);
for (int i = n - 1; i >= 0; i--)
inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
}
int n;
int g(int m) {//循环节长度为 m
int ans = fac[m];
for (int i = 1; i <= k; i++)
if (c[i] % (n / m) != 0)
return 0;
else
ans = 1ll * ans * inv[c[i] / (n / m)] % mod;
return ans;
}
int gcd(int a, int b) {
return a == 0 ? b : gcd(b % a, a);
}
map<int, int> mp;
int f(int m) {
if (mp.count(m))
return mp[m];
int ans = g(m);
for (int i = 1; i * i <= m; i++)
if (m % i == 0) {
if (i != m)
ans = (ans - f(i) + mod) % mod;
if (i * i != m && i != 1)
ans = (ans - f(m / i) + mod) % mod;
}
return mp[m] = ans;
}
int main() {
cin >> k;
n = 0;
for (int i = 1; i <= k; i++)
cin >> c[i], n += c[i];
init(n);
f(n);
int ans = 0;
for (int i = 1; i <= n; i++)
ans = (ans + 1ll * mp[i] * fpow(i, mod - 2, mod) % mod) % mod;
cout << ans << endl;
return 0;
}
Min-max 容斥
公式:
证明:
定义 \(f(n)=\{x|1 \le x \le n, x \in \mathbb{Z}\}\),则 \(f(\max(x,y))=f(x) \bigcup f(y)\),\(f(\min(x,y))=f(x) \bigcap f(y)\)。
于是上面的式子和容斥原理其实是等价的。
首先这东西看上去没啥用,但是它最重要的是对期望也成立:
于是这就可以解决一些期望问题。
题目1: 【Card Collector】hdu-4336
题目2: 【随机游走 (PKUWC’18)】loj-2542
广义容斥原理
全集 \(\Omega\) 中有若干个元素,有 \(n\) 种性质,\(A_i\) 表示满足第 \(i\) 个性质的元素的集合。
设 \(\beta_k\) 表示恰好满足 \(k\) 中性质的元素集合。
定义 \(\alpha_k=\sum_{i_1 < i_2 < \dots < i_k}|A_{i_1} \cap A_{i_2} \cap \dots \cap A_{i_k}|\)。
则 \(\alpha_k = \sum_{i=k}^n \binom{i}{k}\beta_i\)。
理解:
我们考虑实际满足了 \(i\) 种性质的元素在 \(\alpha_k\) 中被算了 \(\binom{i}{k}\) 次,所以这个式子成立。
作用:
根据二项式反演公式3(见二项式反演学习笔记),如果 \(\alpha_k\) 好算,那么我们我们可以反演用 \(\alpha_k\) 求出 \(\beta_k\)。
题目1: 有两个序列 \(a_1 \sim a_n\) 与 \(b_1 \sim b_n\),数字各不相同,求匹配方案数,使得 \(a_i > b_{p_i}\) 的个数恰好为 \(k\)。\(n \le 5000\)(P4859 已经没有什么好害怕的了)
思路:
我们让性质 \(i\) 表示 \(a_i > b_{p_i}\),则我们要求的就是 \(\beta_k\),根据广义容斥原理,我们如果能求出 \(\alpha_k\) 就能解决问题。
考虑如何求 \(\alpha_k\)。数据范围支持 \(O(n^2)\),考虑 dp。
首先需要对两个数组排序。
不妨设 \(f[i][j]\) 表示前 \(i\) 个 \(a\) 中匹配了 \(j\) 个使得满足 \(a_i > b_{p_i}\),所以 \(\alpha_k = (n-k)!f[n][k]\)。
考虑递推,按照第 \(i\) 个分类,可得到 \(f[i][j] = f[i - 1][j] + (cnt[i] - j + 1)f[i - 1][j - 1]\),\(cnt[i]\) 表示有多少个 \(b\) 小于了 \(a_i\)。
第一项好理解,第二项就是 \(i\) 要满足性质,则有 \(cnt[i]\) 个选择,但是前面已经占了 \(j-1\) 个,所以总共只有 \(cnt[i] - (j-1)= cnt[i]-j+1\) 种。
然后就能解决这道问题了。