容斥原理
一、簡介
我們先看一個小問題:
已知站桐亞的有\(a\)人,站桐乃的有\(b\)人,兩個都站的有\(c\)人,問至少站桐亞或者桐乃其中一個的有多少個人?
答案是顯然的:\(a+b-c\),我們可以通過\(Venn\)圖清晰地看出答案:

設站桐亞的集合為\(S_1\),站桐乃的集合為\(S_2\),於是我們有:
那如果不止兩個集合呢?
定理1:對於集合\(S_1,S_2,S_3...S_n\),它們的並集的元素個數是:
證明:
我們可以考慮一個屬於\(\bigcup^{n}_{i=1}\)的元素\(x\)。
令\(x\)所屬的\(m\)個集合為\(T_1,T_2, ...T_m\),其中\(T_i\)是集合\(S_1,S_2,...S_n\)中的任意一個
我們可以通過上列等式右邊的式子得到\(x\)的出現次數\(cnt\)為:
上試結合二項式定理不難理解
於是每個元素在右式中都計算且只計算了1次,所以公式得證
(容斥原理也可以同樣用數學歸納法嚴格證明,這里不再贅述)
根據定理1,我們就可以把求並集元素個數轉化成求交集元素個數
我們令\(\bar{S_i}\)表示\(S_i\)關於全集\(U\)的補集,則我們有:
定理2:
這個定理結合\(Venn\)圖很容易理解,此處不再證明。
根據定理2,我們就可以把求交集元素個數轉化成求並集元素個數
二、容斥原理在一些數學問題上的應用
1.已知不定方程\(x_1+x_2+x_3+...+x_n=m\)和\(n\)個限制條件\(x_i \leq b_i\),求該不定方程的非負數解的數目
我們先不考慮限制條件。那么方程解的數目即為\(C_{n+m-1}^{m-1}\) (插板法)
在應用容斥原理前,我們先確定全集\(U\)以及\(U\)中每個元素的性質\(P_i\)。
於是我們得到:
1.全集\(U\)為滿足該方程組所有非負整數解
2.對於每一個\(x_i\),都有個性質\(P_i\),即\(x_i\leq b_i\)
設\(S_i\)為滿足性質\(P_i\)的集合,那我們的最終答案就是\(|\bigcap _{i=1}^nS_i |\)。
由定理2得:\(|\bigcap _{i=1} ^nS_i |= |U|-|\bigcup _{i=1} ^ n \bar{S_i}|\)
顯然,\(|U|\)即為\(C_{n+m-1}^{m-1}\) ,我們只要計算后半部分即可,而后半部分回歸了容斥原理得一般形式,即后半部分可以用定理1展開!
觀察定理1等式的右半邊,我們只需要考慮以下問題:
給出\(1\leq i_1<i_2<i_3 <...<i_t \leq n\),求\(\bigcap_{k=1}^t \bar{S_{i_k}}\) 的值
現在我們來考慮\(\bar{S_{i_k}}\)的含義。
集合\(\bar{S_{i_k}}\)表示所有滿足\(x_{i_k}\ge b_{i_k}+1\)的解,這說明,有部分變量是有下界限制的
我們現在盡可能的去掉這個限制
於是我們可以將\(m\)減去\(\sum_{k=1}^{t} b_{i_k}+1\)
顯然,新的方程的解與我們要求的解是一一對應的。此時,新方程每個變量都沒有上下界限制
於是,我們可以對集合\(\bar{S_1},\bar{S_2},\bar{S_3}...\bar{S_n}\)按照定理1進行容斥原理的計算
2.(錯排問題)求對於序列\(a=\left\{1,2,3...n\right\}\)的所有排列中,滿足\(a_i \not= i\)的排列的個數
像上一道題目一樣,我們仍然考慮全集\(U\)以及性質\(P\)
1.全集\(U\)為\(a\)的所有排列
2.性質\(P_i\)為\(a_i\not= i\)
設\(S_i\)為滿足性質\(P_i\)的集合,那我們的最終答案就是\(|\bigcap _{i=1}^nS_i |\)。
由定理2得:\(|\bigcap _{i=1} ^nS_i |= |U|-|\bigcup _{i=1} ^ n \bar{S_i}|\)
(和上道題一模一樣)
顯然,\(|U|=A_n^n =n!\)
后半部分一樣可以用定理1展開。
同樣,我們考慮\(\bigcap_{i=1}^{t} \bar{S_{i_k}}\)。
對於每一個\(\bar{S_{i_k}}\),實際上都有一個位置被確定了,而剩下的位置我們可以隨便亂排
於是\(\bigcap_{i=1}^{t} \bar{S_{i_k}}=(n-t)!\)
對於每一個滿足\(1\leq t\leq n\)的\(t\),我們所枚舉的\(\bar{S_{i_1}},\bar{S_{i_2}},\bar{S_{i_3}},...\bar{S_{i_t}}\)對答案的貢獻是一樣的。
於是答案即為:
當然,錯排問題還有遞推的解法,這里不再贅述
3.歐拉函數與莫比烏斯函數
歐拉函數\(\varphi(n)\)表示\(1\)到\(n\)之間與\(n\)互質數的個數
兩個數互質即代表它們的最大公因數為\(1\)
進一步說,任何一個數\(N\)可以被分解成:(唯一分解定理)
其中\(p_1,p_2,p_3,...,p_n\)為質數
也就是說,與\(n\)互質的數,必然沒有\(n\)經過唯一分解之后的質因數
這是,容斥原理出場了:
全集\(U\)表示\(1\)到 \(n\)之間的正整數,性質\(P_i\)表示該數不含有質因數\(p_i\)
設\(S_i\)為滿足性質\(P_i\)的集合,那我們的最終答案就是\(|\bigcap _{i=1}^nS_i |\)。
根據定理2,我們可以得到:(其中t表示唯一分解之后n的質因數個數)
在我們上述求歐拉函數的過程中,我們討論的是一個關於質數的集合。
當我們取遍這個集合的子集的時候,得到的質數的乘積把它唯一分解之后,每個質因數的次數都是1
(由於\(n\)可以唯一分解為\(p_1^{a1}\times p_2^{a_2} \times p_3^{a_3} \times ...\times p_t^{a_t}\),所以事實上\(n\)除以一些質數的成績也是另外一些質數的積)
我們稱這樣的數為無平方因子數。
觀察上式,我們發現僅有\(1\)和無平方因子數對答案有貢獻。
而且,對於一個無平方因子數,它對答案的貢獻取決於它的質因子的個數
我們定義函數\(\mu(n)\)為該數對答案的貢獻,於是我們可以得到:
事實上,這就是著名的莫比烏斯函數
有了莫比烏斯函數,上述歐拉函數的計算公式就可以寫成:
有了莫比烏斯函數,我們就可以在某些數論的計數題中,通過觀察約數對答案的貢獻,利用莫比烏斯函數進行容斥
關於莫比烏斯函數,以下還有幾點想說的:
1.莫比烏斯函數是積性函數,可以用線性篩求解
2.提到莫比烏斯函數,就不得不提莫比烏斯反演:
若有
則有
莫比烏斯反演不是我們討論的重點,有興趣的可以自行了解更多。
4.概率論
對於概率空間內的事件\(A_1,A_2,A_3,...,A_n\),我們有:
若事件的概率只與事件的數量有關,設\(i\)個事件交集的概率為\(a_i\),則:
三、容斥原理在一些信息學競賽的題目里的應用
1.[HAOI2008]硬幣購物
現在有四種面值的硬幣\(c_1,c_2,c_3,c_4\),每一種硬幣有\(d_i\)個, 現在有\(n\)次詢問,每次詢問能用多少種方法來付\(s\)元?
數據范圍:\(n\leq10^3, s\leq10^5\)
這道題看似是一個背包,但單次查詢最好情況下是\(O(4s)\)的,無法接受多組詢問
觀察到題目里只有四種面值,我們於是很容易想到一個不定方程:
\(c_1x_1+c_2x_2+c_3x_3+c_4x_4=s\)
我們一樣的考慮全集\(U\)和性質\(P\):
1.全集\(U\)為不定方程的所有非負整數解
2.\(P_i\)為\(x_i\leq d_i\)
設\(S_i\)為滿足性質\(P_i\)的集合,那我們的最終答案就是\(|\bigcap _{i=1}^nS_i |\)。
由定理2得:\(|\bigcap _{i=1} ^nS_i |= |U|-|\bigcup _{i=1} ^ n \bar{S_i}|\)
(這兩句話又出現了)
顯然,\(\bar{S_i}\) 為滿足\(x_i\ge d_i+1\)的所有不定方程的解
我們仍然可以減去下界和,於是這道題就變成了一個沒有限制的無限背包問題,我們可以進行預處理
設最大的\(s\)為\(m\),則總的時間復雜度為\(O(4m+n\times2^4)\)
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int max_state=1<<4;
int c[5];
int d[5];
ll dp[maxn];
int q,s;
int main(){
for(int i=1;i<=4;i++) scanf("%d",&c[i]);
scanf("%d",&q);
dp[0]=1;
for(int i=1;i<=4;i++)//完全背包預處理
for(int j=c[i];j<=100000;j++)
dp[j]+=dp[j-c[i]];
while(q--){
for(int i=1;i<=4;i++) scanf("%d",&d[i]);
scanf("%d",&s);
ll ans=dp[s];//初始值|U|
for(int i=1;i<max_state;i++){//枚舉每一個集合
int tmp=i;
int res=s;
bool flag=true;
int cnt=0;
while(tmp){
cnt++;
if(tmp&1){
flag^=1;
res-=(d[cnt]+1)*c[cnt];//減掉下界
}
tmp>>=1;
}
if(res>=0) ans=flag?ans+dp[res]:ans-dp[res];//容斥
}
printf("%lld\n",ans);
}
return 0;
}
2.P5339 [TJOI2019]唱、跳、rap和籃球
現在有四類人:一部分最喜歡唱、一部分最喜歡跳、一部分最喜歡rap,還有一部分最喜歡籃球。如果隊列中\(k\),\(k + 1\),\(k + 2\),\(k + 3\)位置上的同學依次,最喜歡唱、最喜歡跳、最喜歡rap、最喜歡籃球,那么他們就會聚在一起討論蔡徐坤。現在我們不希望它們討論蔡徐坤,問一共有多少種方案?
本題有人數限制,我們先把它放一邊
很明顯,這樣我們可以做容斥。
我們可以計算至少有0組討論cxk,至少有一組討論cxk......
那么根據定理2,最后的答案就是:
\(num[0組討論]-num[1組討論]+num[2組討論]+...+(-1)^nnum[全部討論]\)
然后我們把討論cxk的插入進其他人中間。
設有\(t\)組討論cxk,還剩\(r\)個人,那么方案數就是\(C_{r+t}^t\)
現在我們考慮人數限制。
最粗暴的方法就是大力枚舉有多少個人分別唱,跳,rap,打籃球,再加上容斥,\(O(n^4)\),超時
因為只有4種,我們可以巧一點。
我們可以枚舉有\(m\)個唱或跳,那么答案就是:
我們驚訝的發現,\(\sum_{i=m-b}^{a}C_{m}^{i}\) 和\(\sum_{i=n-m-d}^{c}C_{n-m}^{i}\) 可以用前綴和計算
於是我們只需要枚舉\(m\)了
Code:
#include<bits/stdc++.h>
#define MOD 998244353
using namespace std;
typedef long long ll;
const int maxn=1e3+5;
int n,a,b,c,d;
ll C[maxn][maxn];
ll sum[maxn][maxn];
ll ans;
ll res;
inline ll query(int t,int l,int r){
if(l>r) return 0;
if(l<=0) return sum[t][r];
ll tmp=sum[t][r]-sum[t][l-1];
if(tmp<0) res+=MOD;
return tmp;
}
int main(){
scanf("%d%d%d%d%d",&n,&a,&b,&c,&d);
C[0][0]=1; C[1][0]=C[1][1]=1;
for(int i=2;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
}
sum[0][0]=1;
for(int i=0;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=C[i-1][j-1]+C[i-1][j];
if(C[i][j]>=MOD) C[i][j]-=MOD;
}
}
for(int i=0;i<=n;i++){
sum[i][0]=1;
for(int j=1;j<=n;j++){
sum[i][j]=sum[i][j-1]+C[i][j];
if(sum[i][j]>=MOD) sum[i][j]-=MOD;
}
}
a=min(a,n);
b=min(b,n);
c=min(c,n);
d=min(d,n);
int cnt=0;
while(n>=0&&a>=0&&b>=0&&c>=0&&d>=0){
res=0;
for(int i=0;i<=n;i++){
(res+=C[n][i]*query(i,i-b,a)%MOD*query(n-i,n-i-d,c)%MOD)%=MOD;
// cout<<C[n][i]<<' '<<query(i,i-b,a)<<' '<<query(n-i,n-i-d,c)<<endl;
}
(res*=C[n+cnt][cnt])%=MOD;
if(cnt&1) (ans-=res)%=MOD;
else (ans+=res)%=MOD;
n-=4; a--; b--; c--; d--; cnt++;
if(ans<0) ans+=MOD,ans%=MOD;
//cout<<ans<<endl;
}
printf("%lld\n",ans);
return 0;
}
3.[ZJOI2016]小星星
現在給你一顆樹和一個圖,將樹上的點重新編號,使得樹上的一條邊\((u,v)\)在圖上也有一條相應的邊\((idx_u,idx_v)\)(\(idx_i\)表示樹上一點\(i\)重新編號后的結果),問有多少種編號的方法?
(\(n\leq 17 , m\leq n(n-1)/2\))
顯然,本題是一道動態規划題,而且是一道樹形dp。
按照套路,一般樹形dp的第一位肯定這個節點的編號
我們慢慢來分析這個問題:
影響答案的首先是對樹上一個點的重新編號,很容易想到枚舉一個點重新編排后的編號
第二,樹上的一條邊能否對應圖上的一條邊是能否對答案產生貢獻的必要條件
所以對於樹上一點,我們可以枚舉與他相鄰的節點的重新編排后的編號
最后,重新編排的編號不能有重復
於是不難想到一種dp:
令\(dp[i][j][S]\) 表示樹上一點\(i\),重新編號后對應的是\(j\),\(i\)的子樹集合為\(S\)。
因為我們要枚舉一個數子樹的子集,所以復雜度肯定是特別高的。
等一下,為什么我們好端端的枚舉起集合來了?
別忘了,我們要避免重復,即我們希望得到一個1-n的排列
那如果我們不考慮這個條件呢?
這樣dp只有二維,但是有可能會重復
重復了怎么辦?容斥原理
我們強制樹上每個點重新編排后的編號為\(S=\left\{1,2,3,...,n\right\}\)的一個子集,每次我們做一個\(O(n^3)\)的dp,然后枚舉它的子集。
那么答案即為:
\(num[|S|=n]-num[|S|=n-1]+num[|S|=n-2]+...+(-1)^nnum[|S|=1]\)
其中\(num\)表示中括號內值為真時的方案數
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1005;
int n,m;
int g[maxn][maxn];
struct Node{
int to;
int next;
}edge[maxn<<1];
int head[maxn],cnt;
int maxstate;
int tot;
int idx[maxn];
ll dp[maxn][maxn];
ll ans;
inline void add(int x,int y){
edge[++cnt].next=head[x];
edge[cnt].to=y;
head[x]=cnt;
}
inline void dfs(int x,int fa){
//cout<<x<<' '<<fa<<endl;
for(int i=head[x];i!=0;i=edge[i].next){
int k=edge[i].to;
if(k==fa) continue;
dfs(k,x);
}
for(int i=1;i<=tot;i++){
dp[x][idx[i]]=1;
for(int j=head[x];j!=0;j=edge[j].next){
int k=edge[j].to;
if(k==fa) continue;
ll sum=0;
for(int l=1;l<=tot;l++)
if(g[idx[i]][idx[l]]) sum+=dp[k][idx[l]];
//cout<<sum<<endl;
dp[x][idx[i]]*=sum;
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
g[x][y]=g[y][x]=1;
}
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
maxstate=1<<n;
for(int i=0;i<maxstate;i++){
int tmp=i;
int num=0;
tot=0;
while(tmp){
num++;
if(tmp&1) idx[++tot]=num;
tmp>>=1;
}
dfs(1,0);
for(int i=1;i<=tot;i++)
if((n-tot)&1) ans-=dp[1][idx[i]];
else ans+=dp[1][idx[i]];
}
printf("%lld\n",ans);
return 0;
}
參考文獻:
1.2013國家集訓隊論文 王迪《淺談容斥原理》
2.李煜東《算法競賽進階指南》
3.《奧數教程》高中第三分冊
如有不足敬請指正,謝謝!
