容斥原理
基本概念
容斥原理
在計數時,必須注意沒有重復,沒有遺漏。為了使重疊部分不被重復計算,人們研究出一種新的計數方法,這種方法的基本思想是:先不考慮重疊的情況,把包含於某內容中的所有對象的數目先計算出來,然后再把計數時重復計算的數目排斥出去,使得計算的結果既無遺漏又無重復,這種計數的方法稱為容斥原理。(摘自百度百科)
舉個栗子
- 三個集合:
三個集合\(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;
}