容斥原理
基本概念
容斥原理
在计数时,必须注意没有重复,没有遗漏。为了使重叠部分不被重复计算,人们研究出一种新的计数方法,这种方法的基本思想是:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。(摘自百度百科)
举个栗子
- 三个集合:
三个集合\(s_{1},s_{2},s_{3}\), 目标是求解\(s_{1} \cup s_{2} \cup s_{3}\)
易知:
- 四个集合:
四个集合:\(s_{1},s_{2},s_{3}, s_{4}\), 目标是求解:\(s_{1} \cup s_{2} \cup s_{3} \cup s_{4}\)
那么:
容斥原理公式
\(\lvert A_{1} \cap A_{2} \cap A_{3} \cap ... A_{m}\lvert =\)
规律:
奇数项为正, 偶数项为负.
例题
AcWing 890. 能被整除的数
题意
给定\(m\)个质数, 问\(1 - n\)中有多少个数能至少被\(m\)个数中的一个整除
时间复杂度
\(m\)个质因数(即\(m\)个集合), 总共\(2^m\)种组合方式, 每种组合方式对应二进制有\(m\)位
可得时间复杂度:\(O(m\times 2^m)\)
思路
Step 1
对于\(m\)个质数(\(p_{1}, p_{2} ... p_{m}\)), 每个数都存在这样一个集合:
集合\(S_{i}\)代表所有能够被\(p_{i}\)整除的数.
那么根据题意, 我们目标就是求解
中元素的个数
只要应用容斥原理即可.
Step 2
我们发现:
\(\sum_{1 \leqslant i \leqslant m}\lvert A_{i}\lvert\) 是从\(i\)个集合中选择1个, 选法有\(C^{1}_{i}\)种
\(\sum_{1 \leqslant i < j \leqslant m}\lvert A_{i} \cap A_{j} \lvert\) 是从\(i\)个集合内选择2个, 选法有\(C^{2}_{i}\)种.
\(...\)
那么选择容斥原理后, 还有一个问题要解决: 如何枚举公式中的每种情况?
我们采用二进制:
每个二进制数都由\(01\)组成, 那么对于每一位: \(0\)代表选择这个集合, \(1\)代表不选择这个集合
例如\(1010\):代表有四个集合:选择集合\(1,3\), 不选\(2, 4\)
只要依次枚举\(1 - 2^{m} - 1\), 就可以枚举集合的所有选法
(其实: \(C^{0}_{m} + C^{1}_{m} + C^{2}_{m} + ... + C^{m}_{m} = 2^{m}\))
Step 3
如何计算当前集合内能被\(1 - n\)整除的元素个数:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20;
int p[N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i ++ )
cin >> p[i];
int res = 0;
for (int i = 1; i < 1 << m; i ++ ){
int cnt = 0;
int t = 1;
for (int j = 0; j < m; j ++ ){
if (i >> j & 1){ //选择当前集合
cnt ++;
if ((long long)t * p[j] > n){
t = -1;
break;
}
t = (long long)t * p[j];
}
}
if (t != -1){
if (cnt % 2)
res += n / t;
else
res -= n / t;
}
}
cout << res << endl;
return 0;
}