前言
班里有 30 个学生,18 个参加了数学社团,12 个参加了物理社团,5 个两个都参加了。那”至少参加了一个社团”的有多少人?
直觉告诉你:?不对——那 5 个两边都参加的人被算了两次。所以 。
这就是容斥原理。名字听起来高大上,但核心就是一句话:重叠的部分被多算了,要减回来;减多了又要加回来;加多了又要减回来……交替加减直到不重不漏。
00-03 累积和与差分 里二维前缀和的”加了减、减了加”就是最简单的容斥。这篇推广到任意多个集合的情况。
问题的本质
两个集合
两个集合的并 = 各自大小之和 - 交集(被算了两次)。
三个集合
先加三个单独的,减去两两交被多加的,再加回三重交被多减的。符号交替:。
一般公式
翻译成人话:枚举所有非空子集 ,如果 是奇数就加、偶数就减。一共 项。
互补形式
容斥更常用的形式是求都不满足的个数:
即”全集 - 至少满足一个条件的个数”。
理论 + 代码
位掩码枚举所有子集
当集合个数 时,可以用位掩码枚举所有 个子集:
// 容斥公式:枚举所有非空子集
long long ans = 0;
for (int mask = 1; mask < (1 << n); mask++) {
// ① mask 的每一位代表"选不选这个集合"
int bits = __builtin_popcount(mask); // ② 子集大小
long long sz = count_intersection(mask); // ③ 交集的大小
if (bits % 2 == 1)
ans += sz; // ④ 奇数个集合:加
else
ans -= sz; // ⑤ 偶数个集合:减
}
- ②
__builtin_popcount(mask)是 GCC 内置函数,返回mask中 1 的个数(即子集大小)。 - ③
count_intersection(mask)需要根据具体问题实现——计算 mask 中所有集合的交集大小。 - ④⑤ 奇数加偶数减,对应公式中的 。
例题
例题 1:M&A 007 — Number of Multiples 1(★1)
题目: 以下有多少个正整数是 或 的倍数?
数据范围:,
思路:容斥原理最基础的例子。 的倍数有 个, 的倍数有 个,但两者公有的( 的倍数)被算了两次,要减去。
答案 = 。
代码:
#include <cstdio>
using namespace std;
long long gcd(long long a, long long b) {
while (b) { long long t = b; b = a % b; a = t; }
return a;
}
int main() {
long long N, X, Y;
scanf("%lld%lld%lld", &N, &X, &Y);
long long cntX = N / X; // ① X 的倍数个数
long long cntY = N / Y; // ② Y 的倍数个数
long long lcm = X / gcd(X, Y) * Y; // ③ LCM(X,Y),先除后乘防溢出
long long cntXY = N / lcm; // ④ 同时是 X 和 Y 的倍数的个数
printf("%lld\n", cntX + cntY - cntXY); // ⑤ 容斥:|A∪B| = |A|+|B|-|A∩B|
}
逐行解析:
- ⑤ 这就是容斥原理的两集合版本:。
例题 2:M&A 068 — Number of Multiples 2(★2)
题目: 到 中,有多少个整数是 中至少一个的倍数?
数据范围:,,
思路:,可以枚举所有 的子集(共 个),对每个子集求 LCM,然后用容斥原理:
子集大小为奇数时加,偶数时减。
代码:
#include <cstdio>
using namespace std;
long long gcd(long long a, long long b) {
while (b) { long long t = b; b = a % b; a = t; }
return a;
}
int main() {
long long N;
int K;
scanf("%lld%d", &N, &K);
long long V[15];
for (int i = 0; i < K; i++)
scanf("%lld", &V[i]);
long long ans = 0;
for (int mask = 1; mask < (1 << K); mask++) { // ① 枚举所有非空子集
long long l = 1;
int bits = 0;
for (int i = 0; i < K; i++) {
if (mask >> i & 1) {
bits++;
l = l / gcd(l, V[i]) * V[i]; // ② 子集的 LCM
if (l > N) { l = N + 1; break; } // ③ LCM 超过 N 时直接跳出
}
}
if (bits % 2 == 1)
ans += N / l; // ④ 奇数子集:加
else
ans -= N / l; // ⑤ 偶数子集:减
}
printf("%lld\n", ans);
}
逐行解析:
- ①
mask从 1 到 ,每个 mask 代表一个非空子集。 - ② 对子集中的元素求 LCM。先除 GCD 再乘,防溢出。
- ③ 如果 LCM 已经超过 ,则 ,不影响结果,可以剪枝。
- ④⑤ 容斥符号:奇数大小加、偶数大小减。
例题 3:M&A 066 — Three Cards(★2)
题目:有黑、白、灰三张卡片,各写一个 到 的整数。求满足以下至少一个条件的写法数:黑白差 、黑灰差 、灰白差 。
数据范围:,
思路:用容斥求”至少一个条件满足”。总数 ,减去”三个条件都不满足”就是答案。
“三个条件都不满足”= 黑白差 且 黑灰差 且 灰白差 。即所有相邻对的差都 。
直接统计”都不满足”的数量:枚举黑色的值 ( 到 ),白色 必须在 范围内,灰色 必须同时满足和黑色差 及和白色差 。
由于 ,范围很小,可以暴力枚举。
代码:
#include <cstdio>
#include <cstdlib>
using namespace std;
int main() {
long long N;
int K;
scanf("%lld%d", &N, &K);
long long total = N * N % 1000000007 * N % 1000000007; // 总写法 N^3
long long bad = 0;
// 统计"三个条件都不满足"的写法
for (int b = 1; b <= N; b++) {
for (int d_bw = -(K-1); d_bw <= K-1; d_bw++) { // ① 黑白差 < K
int w = b + d_bw;
if (w < 1 || w > N) continue;
for (int d_bg = -(K-1); d_bg <= K-1; d_bg++) { // ② 黑灰差 < K
int g = b + d_bg;
if (g < 1 || g > N) continue;
if (abs(w - g) >= K) continue; // ③ 灰白差也要 < K
bad++;
}
}
}
bad %= 1000000007;
long long ans = (total - bad % 1000000007 + 1000000007) % 1000000007;
printf("%lld\n", ans);
}
参考文献
教材讲解 — 競技プログラミングの鉄則 第 5 章
配套教材
- 「アルゴリズムと数学」書籍 2.5 節「包除原理」
基础练习 — アルゴリズムと数学 演習問題集
- 007 Number of Multiples 1(倍数计数入门)【例题】
- 068 Number of Multiples 2(多集合容斥)【例题】
- 066 Three Cards(有界计数+容斥)【例题】
系列索引
第零章 基础工具
第一章 搜索技术
第二章 数学基础
第三章 数据结构
- 03-01 栈、队列与单调栈
- 03-02 堆与优先队列
- 03-03 并查集
- 03-04 树状数组
- 03-05 线段树
- 03-06 懒标记线段树
- 03-07 Sparse Table 与倍增
- 03-08 字符串哈希
第四章 图论
- 04-01 图的遍历
- 04-02 最短路—Dijkstra 与 01-BFS
- 04-03 最短路—Bellman-Ford 与 Floyd
- 04-04 拓扑排序
- 04-05 最小生成树
- 04-06 强连通分量与 2-SAT
- 04-07 二分图与网络流
- 04-08 树上问题
第五章 动态规划
- 05-01 DP入门—状态与转移
- 05-02 背包问题族
- 05-03 LIS、LCS与编辑距离
- 05-04 区间DP
- 05-05 状态压缩DP
- 05-06 树形DP与数位DP
- 05-07 矩阵快速幂与线性递推
第六章 贪心
第七章 字符串
第八章 进阶