容斥原理
先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。
以S1,S2,S3三个集合为例,求出三个集合中包含的元素个数,可以通过韦恩图得到S1∪S2∪S3 = S1+S2+S3- S1∩S2 - S1∩S3 - S2∩S3 + S1∩S2∩S3。通过数学归纳法可以证明,对于求n个集合S1,S2,…,Sn集合中包含的元素个数,可以通过下面的公式来计算(注意正负号交替):
∣
S
1
∪
S
2
⋯
∪
S
m
∣
=
∑
i
∣
S
i
∣
−
∑
i
,
j
∣
S
i
∩
S
j
∣
+
∑
i
,
j
,
k
∣
S
i
∩
S
j
∩
S
k
∣
.
.
.
.
+
(
−
1
)
n
−
1
∑
∣
S
1
∩
S
2
∩
S
3
.
.
.
∩
S
m
∣
.
.
.
.
.
.
.
.
①
| S_{1}\cup S_{2} \dots \cup S_{m} | = \sum_{i} | S_{i} | - \sum_{i,j} | S_{i} \cap S_{j} |+ \sum_{i,j,k} | S_{i}\cap S_{j} \cap S_{k} |....+(-1)^{n-1}\sum | S_{1}\cap S_{2} \cap S_{3}... \cap S_{m} |........①
∣S1∪S2⋯∪Sm∣=i∑∣Si∣−i,j∑∣Si∩Sj∣+i,j,k∑∣Si∩Sj∩Sk∣....+(−1)n−1∑∣S1∩S2∩S3...∩Sm∣........①
计算总共的项数:利用组合数计算,每次从n个元素里面选i个进行交集计算,故总项数
C
n
1
+
C
n
2
+
⋯
+
C
n
n
=
2
n
C_{n}^{1} + C_{n}^{2} + \dots + C_{n}^{n} = 2 ^ {n}
Cn1+Cn2+⋯+Cnn=2n
即时间复杂度为
O
(
2
n
)
O(2^n)
O(2n)
接下来验证的一下上面①式中每个元素是不是只算了一次(具体证明略):
假设
x
∈
S
1
∪
S
2
⋯
∪
S
n
,存在于
k
个集合之中,
1
≤
k
≤
n
那么
x
被计算的次数为
C
k
1
−
C
k
2
+
C
k
3
−
C
k
4
+
⋯
+
(
−
1
)
k
−
1
C
k
k
=
1
假设x\in S_{1} \cup S_{2} \dots \cup S_{n},存在于k个集合之中,1\le k\le n\\那么x被计算的次数为C_{k}^{1} - C_{k}^{2}+C_{k}^{3}-C_{k}^{4}+ \dots + (-1)^{k-1}C_{k}^{k}=1
假设x∈S1∪S2⋯∪Sn,存在于k个集合之中,1≤k≤n那么x被计算的次数为Ck1−Ck2+Ck3−Ck4+⋯+(−1)k−1Ckk=1
Acwing 890.能被整除的数
实现思路:记Si为1~n中能被pi整除的集合,根据容斥原理,所有数的个数为各个集合的并集,计算公式:
∣
S
1
∪
S
2
⋯
∪
S
m
∣
=
∑
i
∣
S
i
∣
−
∑
i
,
j
∣
S
i
∩
S
j
∣
+
∑
i
,
j
,
k
∣
S
i
∩
S
j
∩
S
k
∣
.
.
.
.
+
(
−
1
)
n
−
1
∑
∣
S
1
∩
S
2
∩
S
3
.
.
.
∩
S
m
∣
| S_{1}\cup S_{2} \dots \cup S_{m} | = \sum_{i} | S_{i} | - \sum_{i,j} | S_{i} \cap S_{j} |+ \sum_{i,j,k} | S_{i}\cap S_{j} \cap S_{k} |....+(-1)^{n-1}\sum | S_{1}\cap S_{2} \cap S_{3}... \cap S_{m} |
∣S1∪S2⋯∪Sm∣=i∑∣Si∣−i,j∑∣Si∩Sj∣+i,j,k∑∣Si∩Sj∩Sk∣....+(−1)n−1∑∣S1∩S2∩S3...∩Sm∣
-
每个集合Si就对应能被质数pi整除的数
-
对于每个集合Si实际上并不需要知道含有哪些元素,只需要知道各个集合中元素个数,对于单个集合Si中元素个数就是对应质数pi的倍数个数(1~n范围内),计算公式为
∣ S i ∣ = n p i ,下取整 |S_i|=\frac{n}{p_i},下取整 ∣Si∣=pin,下取整 -
对于任意个集合交集中元素的个数:每个质数pi对应一个集合Si,那么
∣ S i ∩ S j ∣ = n p i ∗ p j ,下取整,即交集就是 p i 和 p j 的公倍数的个数 |S_i \cap S_j|=\frac{n}{p_i*p_j},下取整,即交集就是p_i和p_j的公倍数的个数 ∣Si∩Sj∣=pi∗pjn,下取整,即交集就是pi和pj的公倍数的个数 -
表示每个集合的状态(即选中几个集合的交集,):m个质数,需要m个二进制位表示,共 2 m − 1 2^m-1 2m−1种情况(至少选中一个集合),个数前面的符号为
(-1)^(n-1)
。以m = 4为例,所以需要4个二进制位来表示每一个集合选中与不选的状态,若此时为1011,表示集合S1∩S3∩S4中元素的个数,同时集合个数为3,前面的符号为(-1)^(3-1)=1,即
∣ S 1 ∩ S 3 ∩ S 4 ∣ = ( − 1 ) 3 − 1 n p 1 ∗ p 3 ∗ p 4 |S_1 \cap S_3 \cap S_4|=(-1)^{3-1}\frac{n}{p_1*p_3*p_4} ∣S1∩S3∩S4∣=(−1)3−1p1∗p3∗p4n怎么取到一个数的每一个二进制位:使用位运算(第一章), 数i
的第j
位是否为1:i >> j & 1
注:用二进制表示状态的小技巧非常常用,后面的状态压缩DP也用到了这个技巧,因此一定要掌握
样例分析:
具体实现代码(详解版):
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N]; // 存储质数p[i]
int n, m;
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) cin >> p[i]; // 输入 m 个质数并存入数组 p 中
int res = 0; // 结果初始化为 0
// 枚举所有质数的子集状态,状态用二进制表示,从 1 到 (1 << m) - 1
// 即从000...01到111...11,其中每一位表示是否选中对应的质数
for (int i = 1; i < 1 << m; i++) {
int t = 1, s = 0; // t 表示当前质数子集的乘积,s 表示选中的质数个数
// 枚举每一个质数,判断它是否在当前子集中
for (int j = 0; j < m; j++) {
// 判断第 j 个质数是否被选中(通过位运算检查第 j 位是否为 1)
if (i >> j & 1) {
// 检查当前乘积是否超过 n,若超过则停止计算该子集
if ((LL)t * p[j] > n) {
t = -1; // 标记当前子集不可行
break;
}
s++; // 记录当前子集中质数的个数
t = (LL)t * p[j]; // 更新子集的质数乘积
}
}
// 如果当前子集不可行(乘积超出范围),则跳过
if (t == -1) continue;
// 根据子集质数个数 s 的奇偶性更新结果:
// 如果质数个数为奇数,则加上 n / t;如果为偶数,则减去 n / t
if (s % 2) res += n / t; // 奇数个质数,容斥原理加上这个集合的贡献
else res -= n / t; // 偶数个质数,容斥原理减去这个集合的贡献
}
cout << res << endl;
return 0;
}
这道题利用了容斥原理,核心思想是通过枚举质数子集并计算其整除数量,来处理多个集合的并集大小问题。通过容斥加减,避免了重复计数。