容斥原理
一、课上练习
编程练习
(暂无)
二、知识总结
✨ 核心思想
容斥原理(Inclusion-Exclusion Principle)是一种精确计数的方法:先把各部分加起来(包含),再减去多算的交集(排斥),再加回多减的部分......如此交替加减,得到精确的计数结果。它是解决"至少满足一个条件"或"恰好不满足任何条件"类问题的利器。
✨ 定理内容
两个集合:
|A ∪ B| = |A| + |B| - |A ∩ B|
三个集合:
|A ∪ B ∪ C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C|
一般形式:设 A₁, A₂, ..., Aₙ 为有限集合,则:
|A₁ ∪ A₂ ∪ ... ∪ Aₙ| = Σ|Aᵢ| - Σ|Aᵢ∩Aⱼ| + Σ|Aᵢ∩Aⱼ∩Aₖ| - ... + (-1)^(n+1)|A₁∩A₂∩...∩Aₙ|
即奇数个集合的交集取正号,偶数个集合的交集取负号。
补集形式:在全集 U 中,不属于任何 Aᵢ 的元素个数:
|U - (A₁∪...∪Aₙ)| = |U| - Σ|Aᵢ| + Σ|Aᵢ∩Aⱼ| - ...
✨ 代码实现
1#include <iostream>
2#include <vector>
3using namespace std;
4typedef long long ll;
5
6// 求 1~n 中与 m 互素的数的个数
7// 思路:先对 m 质因数分解,然后容斥
8ll coprime_count(ll n, ll m) {
9 // 质因数分解 m
10 vector<ll> primes;
11 ll temp = m;
12 for (ll i = 2; i * i <= temp; i++) {
13 if (temp % i == 0) {
14 primes.push_back(i);
15 while (temp % i == 0) temp /= i;
16 }
17 }
18 if (temp > 1) primes.push_back(temp);
19
20 int k = primes.size();
21 ll result = 0;
22
23 // 枚举所有质因子的子集(用位运算)
24 for (int mask = 1; mask < (1 << k); mask++) {
25 ll prod = 1; // 当前子集中质因子的乘积
26 int bits = 0; // 当前子集的大小
27
28 for (int i = 0; i < k; i++) {
29 if (mask & (1 << i)) {
30 prod *= primes[i];
31 bits++;
32 }
33 }
34
35 // 容斥:奇数个质因子取正(减),偶数个取负(加)
36 if (bits % 2 == 1) {
37 result += n / prod; // 能被 prod 整除的数的个数
38 } else {
39 result -= n / prod;
40 }
41 }
42
43 // 总数 - 能被至少一个质因子整除的数 = 与 m 互素的数
44 return n - result;
45}
46
47int main() {
48 ll n, m;
49 cin >> n >> m;
50 cout << "1~" << n << " 中与 " << m << " 互素的数有 "
51 << coprime_count(n, m) << " 个" << endl;
52 return 0;
53}1#include <iostream>
2using namespace std;
3typedef long long ll;
4const int MOD = 1e9 + 7;
5
6// 错排数 D(n):n 个元素的全错位排列数
7// 即没有任何元素在原来位置上的排列数
8// D(n) = n! * Σ(i=0 to n) (-1)^i / i!
9// 递推公式:D(n) = (n-1) * (D(n-1) + D(n-2))
10
11const int MAXN = 1000005;
12ll D[MAXN];
13
14void init_derangement(int n) {
15 D[0] = 1; // 空排列算一种
16 D[1] = 0; // 1个元素无法错排
17 for (int i = 2; i <= n; i++) {
18 D[i] = (ll)(i - 1) % MOD * ((D[i-1] + D[i-2]) % MOD) % MOD;
19 }
20}
21
22int main() {
23 int n;
24 cin >> n;
25 init_derangement(n);
26 cout << "D(" << n << ") = " << D[n] << endl;
27 return 0;
28}1#include <iostream>
2#include <vector>
3#include <algorithm>
4using namespace std;
5typedef long long ll;
6
7// 求 [1, n] 中能被 a[] 中至少一个数整除的数的个数
8ll count_divisible(ll n, vector<ll>& a) {
9 int k = a.size();
10 ll result = 0;
11
12 for (int mask = 1; mask < (1 << k); mask++) {
13 ll lcm_val = 1;
14 int bits = __builtin_popcount(mask);
15
16 for (int i = 0; i < k; i++) {
17 if (mask & (1 << i)) {
18 // 计算 lcm,防止溢出
19 lcm_val = lcm_val / __gcd(lcm_val, a[i]) * a[i];
20 if (lcm_val > n) break; // 超过 n 则贡献为 0
21 }
22 }
23
24 if (bits % 2 == 1) {
25 result += n / lcm_val;
26 } else {
27 result -= n / lcm_val;
28 }
29 }
30
31 return result;
32}
33
34int main() {
35 ll n;
36 int k;
37 cin >> n >> k;
38 vector<ll> a(k);
39 for (int i = 0; i < k; i++) cin >> a[i];
40
41 cout << "[1, " << n << "] 中能被至少一个数整除的有 "
42 << count_divisible(n, a) << " 个" << endl;
43 return 0;
44}✨ 应用示例
示例 1:求 1~100 中能被 2 或 3 或 5 整除的数的个数
设 A = 能被2整除, B = 能被3整除, C = 能被5整除
|A| = 50, |B| = 33, |C| = 20 |A∩B| = |能被6整除| = 16 |A∩C| = |能被10整除| = 10 |B∩C| = |能被15整除| = 6 |A∩B∩C| = |能被30整除| = 3
|A∪B∪C| = 50+33+20 - 16-10-6 + 3 = 74
示例 2:求 1~100 中与 30 互素的数的个数
30 = 2 × 3 × 5,质因子为 {2, 3, 5}
与 30 不互素 = 能被 2 或 3 或 5 整除 = 74(由上题)
与 30 互素的数 = 100 - 74 = 26
示例 3:错排问题
4 个人的信放进 4 个信封,全部放错的方案数:
D(4) = 3 × (D(3) + D(2)) = 3 × (2 + 1) = 9
也可以用容斥:4! - C(4,1)×3! + C(4,2)×2! - C(4,3)×1! + C(4,4)×0! = 24 - 24 + 12 - 4 + 1 = 9
✨ 解题步骤详解
- 定义集合:将"不想要的性质"定义为集合 Aᵢ
- 计算各交集大小:利用问题的结构(如质因数、整除性等)计算 |Aᵢ∩Aⱼ∩...|
- 枚举子集:用位运算枚举所有非空子集
- 交替加减:奇数大小的子集加,偶数大小的减
- 得到结果:
- "至少满足一个":直接是容斥结果
- "一个也不满足":总数减去容斥结果
时间复杂度:O(2^k),其中 k 为集合个数。因此 k 通常不能太大(一般 k ≤ 20)。
✨ 常见错误
- 容斥的符号搞反:奇数个集合的交集应该加,偶数个应该减
- 计算交集时用"乘积"而非"lcm"(当集合是"能被 aᵢ 整除"时,交集应用 lcm)
- 位运算枚举时忘记从 1 开始(mask=0 对应空集,不参与容斥)
- 计算 lcm 时乘法溢出
- 错排递推的初始值搞错(D(0)=1 而不是 0)
- 将补集形式中的符号与标准形式混淆
✨ 算法评价
| 应用 | 时间复杂度 | 说明 |
|---|---|---|
| k 个条件的容斥 | O(2^k) | k 为条件数,一般 ≤ 20 |
| 互素计数 | O(2^ω(m) × √m) | ω(m) 为 m 的不同质因子个数 |
| 错排数 | O(n) | 递推 |
容斥原理是竞赛中使用频率极高的计数工具,与莫比乌斯反演、欧拉函数等有深刻的联系。掌握容斥原理的关键在于:准确定义集合、正确计算交集大小、注意枚举和符号。