容斥原理我初中就聽老師說過了,不知道大家有沒有聽過(/≧▽≦)/php
百度百科說:數組
在計數時,必須注意沒有重複,沒有遺漏。ide
爲了使重疊部分不被重複計算,人們研究出一種新的計數方法。函數
這種方法的基本思想是:先不考慮重疊的狀況,把包含於某內容中的全部對象的數目先計算出來,而後再把計數時重複計算的數目排斥出去,使得計算的結果既無遺漏又無重複。spa
這種計數的方法稱爲容斥原理。3d
好標準的說法(#-.-)code
那我舉個簡單的例子對象
兩個集合的容斥原理: 設A, B是兩個有限集合blog
那麼遞歸
|A + B| = |A| + |B| - |AB|
|A|表示A集合中的元素個數
三個集合的容斥原理: 設A, B, C是三個有限集合
那麼
|A + B + C| = |A| + |B| + |C| - |AB| - |AC| - |BC| + |ABC|
這就叫容斥原理
接下來直接作例題了
全錯排(裝錯信封問題)
hdu 1465
http://acm.hdu.edu.cn/showproblem.php?pid=1465
n封信對應n個信封
求剛好所有裝錯了信封的方案數
原本全錯排是有本身的一個公式的,叫全錯排公式(跟容斥不要緊)
那我順便來說講全錯排( >ω<)
要裝第i封信的時候,先把前i-1個信全裝錯信封,而後隨便選其中一個與第i封信交換,有i-1種選法
那麼dp[i] = (i-1) * dp[i-1]
可是還有一種狀況
要裝第i封信的時候,先從i-1封信中任選i-2個信把他們全裝錯信封,而後把剩下的那個信與第i個交換,從i-1封信中任選i-2個信有i-1種選法
那麼dp[i] = (i-1) * dp[i-2]
兩個式子聯合起來
就是那麼dp[i] = (i-1) * (dp[i-1] + dp[i-2])
這就是全錯排公式,遞推,遞歸均可以作
全錯排遞推AC代碼:
1 #include<cstdio> 2 typedef long long LL; 3 int n; 4 LL dp[25]; 5 void init(){ 6 dp[1] = 0; 7 dp[2] = 1; 8 for(int i = 3; i <= 20; i ++){ 9 dp[i] = (i-1) * (dp[i-1] + dp[i-2]); 10 } 11 } 12 int main(){ 13 init(); 14 while(~scanf("%d", &n)){ 15 printf("%I64d\n", dp[n]); 16 } 17 }
那麼這題容斥怎麼作呢?
首先,全部裝信的總數是n!
(在n中任選一個信封放進一封信,而後在剩下的n-1中任選一個信封放進一封信,以此類推,因此是n*(n-1)*(n-2)... = n!)
假設
A1表示1封信裝對信封,數量是(n-1)! (只有n-1個位置能夠亂放)
A2表示2封信裝對信封,數量是(n-2)! (只有n-2個位置能夠亂放)
...
An表示n封信裝對信封,數量是1
那麼這題的答案就是
n! - C(n, 1)*|A1| + C(n, 2)*|A2| - C(n, 3)*|A3| + ... + (-1)^n * C(n, n)*|A4|
把C(n, m)用
代入式子
化簡
n! - n! / 1! + n! / 2! - n! / 3! + ... + (-1)^n * n! / n!
提取n!
n!(1 - 1/1! + 1/2! - 1/3! + ... + (-1)^n * 1/n!)
附上容斥AC代碼:
1 #include<cstdio> 2 typedef long long LL; 3 int n, flag; 4 LL fac[25]; 5 LL ans; 6 void init(){ 7 fac[0] = 1; 8 for(int i = 1; i <= 20; i ++) fac[i] = fac[i-1] * i; 9 } 10 int main(){ 11 init(); 12 while(~scanf("%d", &n)){ 13 ans = fac[n]; 14 flag = -1;//容斥的符號變化 15 for(int i = 1; i <= n; i ++){ 16 ans += flag * fac[n] / fac[i]; 17 flag = -flag; 18 } 19 printf("%I64d\n", ans); 20 } 21 }
第二例題:
UVALive 7040
https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=5052
題意:給n盆花塗色,從m種顏色中選取k種顏色塗,保證正好用上k種顏色,你必須用上這k種顏色去塗滿n個相鄰的花,而且要求相鄰花的顏色不一樣,求方案數。
(1 ≤ n, m ≤ 1e9 , 1 ≤ k ≤ 1e6 , k ≤ n, m)
首先,用k種顏色塗花,假如不考慮所有用上,那麼總的方案數是多少
第一盆花有k種顏色選擇,以後的花由於不能跟前一盆花的顏色相同,因此有k-1種選擇
因而總方案數爲k*(k-1)^(n-1)
由於題目問必須用上k種顏色
這裏麪包含了只用k-1種顏色的狀況,應該減掉全部用k-1種的狀況
減掉的東西里面,這裏麪包含了只用k-2種顏色的狀況,應該加回來
...
反反覆覆,最後就得出答案了(這算是解釋嗎。。。)
最後答案就是
C(m,k) * ( k * (k-1)^(n-1) + [∑((-1)^i * C(k, k - i) * (k-i) * (k-i-1)^(n-1)) ] ) (1 <= i <= k-1) 紅色表示容斥部分
(這裏m有1e9,C(m, k)直接用for循環算,直接for循環從m*(m-1)*...*(m-k+1)再乘k的階乘的逆元)
AC代碼:
1 #include<cstdio> 2 typedef long long LL; 3 const int N = 1000000 + 5; 4 const int MOD = (int)1e9 + 7; 5 int F[N], Finv[N], inv[N]; 6 LL pow_mod(LL a, LL b, LL p){ 7 LL ret = 1; 8 while(b){ 9 if(b & 1) ret = (ret * a) % p; 10 a = (a * a) % p; 11 b >>= 1; 12 } 13 return ret; 14 } 15 void init(){ 16 inv[1] = 1; 17 for(int i = 2; i < N; i ++){ 18 inv[i] = (MOD - MOD / i) * 1ll * inv[MOD % i] % MOD; 19 } 20 F[0] = Finv[0] = 1; 21 for(int i = 1; i < N; i ++){ 22 F[i] = F[i-1] * 1ll * i % MOD; 23 Finv[i] = Finv[i-1] * 1ll * inv[i] % MOD; 24 } 25 } 26 int comb(int n, int m){ 27 if(m < 0 || m > n) return 0; 28 return F[n] * 1ll * Finv[n - m] % MOD * Finv[m] % MOD; 29 } 30 int main(){ 31 init(); 32 int T, n, m, k, ans, flag, temp; 33 scanf("%d", &T); 34 for(int cas = 1; cas <= T; cas ++){ 35 scanf("%d%d%d", &n, &m, &k); 36 ans = k * pow_mod(k-1, n-1, MOD) % MOD; 37 flag = -1; 38 //計算容斥 39 for(int i = 1; i <= k-1; i ++){ 40 ans = (ans + 1ll * flag * comb(k, k-i) * (k-i) % MOD * pow_mod((k-i-1), n-1, MOD) % MOD) % MOD; 41 flag = -flag; 42 } 43 //接下來計算C(m, k) 44 temp = Finv[k]; 45 for(int i = 1; i <= k; i ++){ 46 temp = 1ll * temp * (m-k+i) % MOD; 47 } 48 ans = ((1ll * ans * temp) % MOD + MOD) % MOD; 49 printf("Case #%d: %d\n", cas, ans); 50 } 51 }
第三例題:(容斥這章的例題我可能會寫不少(o^∇^o)ノ預祝玩的開心have fun)
hdu 4135
http://acm.hdu.edu.cn/showproblem.php?pid=4135
題意:就是讓你求(a,b)區間與n互質的數的個數.
咱們能夠先求(1~b)區間的答案,而後減去(1~a-1)區間的答案
這樣問題就轉換爲(1~m)區間與n互質的數的個數
互質的很差求,咱們能夠求不互質的個數,而後減一下
全部咱們先求出n的全部質因數,而後用容斥作
AC代碼:
1 #include<cstdio> 2 #include<vector> 3 using namespace std; 4 typedef long long LL; 5 vector <LL > prime_factor; 6 vector <LL > vec; 7 void init(LL x){ 8 //預處理質因子 9 prime_factor.clear(); 10 for(LL i = 2; i*i <= x; i++){ 11 if(x % i == 0){ 12 prime_factor.push_back(i); 13 while(x % i == 0) x /= i; 14 } 15 } 16 if(x > 1) prime_factor.push_back(x); 17 //預處理容斥中的倍數項,符號正好是一個減一個加 18 int vec_size; 19 vec.clear(); 20 for(int i = 0; i < prime_factor.size(); i ++){ 21 vec_size = vec.size();//由於vec.size()在接下來的運算中會改變 22 for(int j = 0; j < vec_size; j ++){ 23 vec.push_back(vec[j] * prime_factor[i]); 24 } 25 vec.push_back(prime_factor[i]); 26 } 27 } 28 LL work(LL x){ 29 //接下來容斥 30 LL ans = x, flag = -1; 31 for(int i = 0; i < vec.size(); i ++){ 32 ans += flag * x / vec[i]; 33 flag = -flag; 34 } 35 return ans; 36 } 37 int main(){ 38 int T; 39 LL l, r, n; 40 scanf("%d", &T); 41 for(int cas = 1; cas <= T; cas ++){ 42 scanf("%I64d%I64d%I64d", &l, &r, &n); 43 init(n); 44 printf("Case #%d: %I64d\n", cas, work(r) - work(l-1)); 45 } 46 }
容斥中的那些倍數我是這麼處理的
好比30 = 2 * 3 * 5
一開始數組裏面什麼都沒有
而後變成
2
而後把3挨個乘過去的值放在數組後面,同時將本身也放進數組
2 6 3
而後5也是同樣
2 6 3 10 30 15 5
最後答案n就是等於
n - n / 2 + n / 6 - n / 3 + n / 10 - n / 30 + n / 15 - n / 5
固然,除了數組形式,還能夠用位運算來實現容斥
AC代碼:
1 #include<cstdio> 2 #include<vector> 3 using namespace std; 4 typedef long long LL; 5 vector <LL > prime_factor; 6 void init(LL x){ 7 //預處理質因子 8 prime_factor.clear(); 9 for(LL i = 2; i*i <= x; i++){ 10 if(x % i == 0){ 11 prime_factor.push_back(i); 12 while(x % i == 0) x /= i; 13 } 14 } 15 if(x > 1) prime_factor.push_back(x); 16 } 17 LL work(LL x){ 18 //接下來容斥 19 LL ans = x, cnt, temp; 20 for(int i = 1; i < (1 << prime_factor.size()); i ++){ 21 cnt = 0; 22 temp = 1; 23 for(int j = 0; j < prime_factor.size(); j ++){ 24 if(i & (1 << j)){ 25 temp *= prime_factor[j]; 26 cnt ++; 27 } 28 } 29 if(cnt & 1) ans -= x / temp; 30 else ans += x / temp; 31 } 32 return ans; 33 } 34 int main(){ 35 int T; 36 LL l, r, n; 37 scanf("%d", &T); 38 for(int cas = 1; cas <= T; cas ++){ 39 scanf("%I64d%I64d%I64d", &l, &r, &n); 40 init(n); 41 printf("Case #%d: %I64d\n", cas, work(r) - work(l-1)); 42 } 43 }
第四例題:
hdu 1695
http://acm.hdu.edu.cn/showproblem.php?pid=1695
題意:給你5個數a,b,c,d,k
在a~b中選一個x, c~d中選一個y,知足gcd(x,y) = k , 求(x,y) 的對數
a, b, c, d, k, 0 < a <= b <= 100,000, 0 < c <= d <= 100,000, 0 <= k <= 100,000
在題目描述的最後一行有一句話,多組裏面全部的a和c都是1(這題目不是坑爹嗎(╯‵□′)╯︵┻━┻那輸入a和c有什麼用)
而後題目變成
在1~b中選一個x, 1~d中選一個y,知足gcd(x,y) = k , 求(x,y) 的對數 。。。(無語中。。。)
gcd(x, y) == k 說明x,y都能被k整除, 可是能被k整除的未必gcd=k , 必須還要知足互質關係
那麼問題就轉化爲
求1~b/k 和 1~d/k間,gcd(x,y) = 1對數的問題
假設b/k小於d/k
那麼當y <= b/k時,就是求1到b/k的歐拉函數的和
y > b/k時,只好枚舉y從b/k到d/k,用第3例題的求法
這樣問題就解決了(注意:k能夠等於0,要特判)
AC代碼:
1 #include<cstdio> 2 #include<algorithm> 3 #include<vector> 4 using namespace std; 5 typedef long long LL; 6 const int N = 1e5+10 ; 7 vector <LL > prime_factor; 8 int phi[N], prime[N]; 9 int tot;//tot計數,表示prime[N]中有多少質數 10 void Euler(){ 11 phi[1] = 1; 12 for(int i = 2; i < N; i ++){ 13 if(!phi[i]){ 14 phi[i] = i-1; 15 prime[tot ++] = i; 16 } 17 for(int j = 0; j < tot && 1ll*i*prime[j] < N; j ++){ 18 if(i % prime[j]) phi[i * prime[j]] = phi[i] * (prime[j]-1); 19 else{ 20 phi[i * prime[j] ] = phi[i] * prime[j]; 21 break; 22 } 23 } 24 } 25 } 26 void getFactors(int x){ 27 prime_factor.clear(); 28 for(int i = 0; prime[i] <= x / prime[i]; i ++){ 29 if(x % prime[i] == 0){ 30 prime_factor.push_back(prime[i]); 31 while(x % prime[i] == 0) x /= prime[i]; 32 } 33 } 34 if(x > 1) prime_factor.push_back(x); 35 } 36 LL work(int n, int m){ 37 LL ans = n, cnt, temp; 38 getFactors(m); 39 for(int i = 1; i < (1 << prime_factor.size()); i ++){ 40 cnt = 0; 41 temp = 1; 42 for(int j = 0; j < prime_factor.size(); j ++){ 43 if(i & (1 << j)){ 44 temp *= prime_factor[j]; 45 cnt ++; 46 } 47 } 48 if(cnt & 1) ans -= n / temp; 49 else ans += n / temp; 50 } 51 return ans; 52 } 53 int main(){ 54 Euler(); 55 int T, a, b, c, d, k; 56 LL ans; 57 scanf("%d", &T); 58 for(int cas = 1; cas <= T; cas ++){ 59 scanf("%d%d%d%d%d", &a, &b, &c, &d, &k); 60 if(k == 0){ 61 printf("Case %d: 0\n", cas); 62 continue; 63 } 64 if(b > d) swap(b, d);//假設b<=d 65 b /= k; d /= k; 66 ans = 0; 67 for(int i = 1; i <= b; i ++) ans += phi[i]; 68 for(int i = b + 1; i <= d; i ++) ans += work(b, i); 69 printf("Case %d: %I64d\n", cas, ans); 70 } 71 }
這題時間只能算卡過去的,由於正常計算下來,這樣的代碼會超時,只是數據水
這題正確的作法應該是莫比烏斯反演,咱們之後會講到
容來容去,腦子都亂了。。。。
>﹏<