1. 程式人生 > 實用技巧 >容斥原理學習筆記

容斥原理學習筆記

獲得更好的閱讀體驗,請開啟夜間模式

定義

在計數時,必須注意沒有重複,沒有遺漏。為了使重疊部分不被重複計算,人們研究出一種新的計數方法,這種方法的基本思想是:先不考慮重疊的情況,把包含於某內容中的所有物件的數目先計算出來,然後再把計數時重複計算的數目排斥出去,使得計算的結果既無遺漏又無重複,這種計數的方法稱為容斥原理

一、普通容斥

公式

\(U\) 中元素有 \(n\) 種不同的屬性,而第 \(i\) 種屬性稱為 \(P_i\),擁有屬性\(P_i\) 的元素構成集合\(S_i\) ,那麼

\[\begin{split} \left|\bigcup_{i=1}^{n}S_i\right|=&\sum_{i}|S_i|-\sum_{i<j}|S_i\cap S_j|+\sum_{i<j<k}|S_i\cap S_j\cap S_k|-\cdots\\ &+(-1)^{m-1}\sum_{a_i<a_{i+1} }\left|\bigcap_{i=1}^{m}S_{a_i}\right|+\cdots+(-1)^{n-1}|S_1\cap\cdots\cap S_n| \end{split} \]

\[\left|\bigcup_{i=1}^{n}S_i\right|=\sum_{m=1}^n(-1)^{m-1}\sum_{a_i<a_{i+1} }\left|\bigcap_{i=1}^mS_{a_i}\right| \]

大概的思想就是奇加偶減,用單個元素之和減去兩個集合相交的部分,再減去三個集合相交的部分,再加上四個集合相交的部分

證明

對於每個元素使用二項式定理計算其出現的次數。對於元素 \(x\),假設它出現在\(T_1,T_2,T_3...\)的集合中,那麼它的出現次數為

\[\begin{split} Cnt=&|\{T_i\}|-|\{T_i\cap T_j|i<j\}|+\cdots+(-1)^{k-1}\left|\left\{\bigcap_{i=1}^{k}T_{a_i}|a_i<a_{i+1}\right\}\right|\\ &+\cdots+(-1)^{m-1}|\{T_1\cap\cdots\cap T_m\}|\\ =&C_m^1-C_m^2+\cdots+(-1)^{m-1}C_m^m\\ =&C_m^0-\sum_{i=0}^m(-1)^iC_m^i\\ =&1-(1-1)^m=1 \end{split} \]

以上引用自OI-WIKI

例題一:八

題目描述

八是個很有趣的數字啊。
八=發,八八=爸爸,\(88\)=拜拜。
當然最有趣的還是 \(8\)
用二進位制表示是 \(1000\)
怎麼樣,有趣吧。當然題目和這些都沒有關係。
某個人很無聊,他想找出 \([a,b]\)中能被 \(8\)整除卻不能被其他一些數整除的數。

輸入格式

第一行一個數 \(n\),代表不能被整除的數的個數。
第二行 \(n\)個數,中間用空格隔開。
第三行兩個數 \(a,b\),中間一個空格。

輸出格式

一個整數,為 \([a,b]\)間能被 整除卻不能被那 \(n\)個數整除的數的個數。

樣例

樣例輸入

3
7764 6082 462
2166 53442

樣例輸出

6378

資料範圍與提示

對於 \(30\%\)的資料, \(1⩽n⩽5,1⩽a⩽b⩽10^5\)
對於 \(100\%\)的資料,\(1⩽n⩽15,1⩽a⩽b⩽10^9\)\(n\)個數全都小於等於 \(10^4\) 大於等於 \(1\)

分析

我們先算出 \([l,r]\) 中能被 \(8\) 整除的數的個數,再減去能被 \(8\)\(n\) 個數中任意一個數整除的數的個數,再加上能被 \(8\)\(n\) 個數中任意兩個數整除的數的個數,依此類推

程式碼

#include<cstdio>
#define rg register
const int maxn=18;
int a[maxn],n,l,r,mmax,ans;
long long gcd(long long aa,long long bb){
	if(bb==0) return aa;
	return gcd(bb,aa%bb);
}
long long lcm(long long aa,long long bb){
	return 1LL*aa/gcd(aa,bb)*bb;
}
int main(){
	scanf("%d",&n);
	for(rg int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	scanf("%d%d",&l,&r);
	mmax=(1<<n)-1;
	ans=r/8-(l-1)/8;
	rg long long now;
	rg int cnt;
	for(int i=1;i<=mmax;i++){
		now=8,cnt=0;
		for(rg int j=1;j<=n;j++){
			if(i&(1<<(j-1))){
				now=lcm(now,(long long)a[j]);
				if(now>r) break;
				cnt++;
			}
		}
		if(cnt&1) ans-=(1LL*r/now-1LL*(l-1)/now);
		else ans+=(1LL*r/now-1LL*(l-1)/now);
	}
	printf("%d\n",ans);
	return 0;
}

例題二、建設城市

題目描述


分析

如果我們不考慮最多選 \(k\) 個施工隊的限制的話,那麼總方案數為 \(C_{m-1}^{n-1}\)(根據隔板法)
現在我們要做的就是求出不滿足限制的方案數
我們可以分別算出至少有 \(i\) 座城市不滿足限制的方案數 \(C_n^i \times C{m-1-k\times i}^{n-1}\)
含義是先從 \(n\) 座城市裡選出 \(i\) 座城市,再從剩下的 \(m-1-k\times i\) 個空位中選出 \(n-1\) 個空位
在根據容斥原理求出不滿足條件的方案數
用總方案數一減就可以了

程式碼

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<iostream>
#define rg register
const int mod=998244353;
const int maxn=1e7+5;
int n,m,k,ny[maxn],jc[maxn],jcc[maxn],ans;
int getC(int nn,int mm){
	return 1LL*jc[nn]*jcc[nn-mm]%mod*jcc[mm]%mod;
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	if(m<n || 1LL*n*k<1LL*m){
		printf("0\n");
		return 0;
	}
	ny[1]=1;
	for(rg int i=2;i<maxn;i++){
		ny[i]=1LL*(mod-mod/i)*ny[mod%i]%mod;
	}
	jc[0]=jcc[0]=1;
	for(rg int i=1;i<maxn;i++){
		jc[i]=1LL*jc[i-1]*i%mod;
		jcc[i]=1LL*jcc[i-1]*ny[i]%mod;
	}
	ans=getC(m-1,n-1);
	for(rg int i=1;i<=n;i++){
		if(m-i*k<n) break;
		if(i&1) ans-=1LL*getC(n,i)*getC(m-i*k-1,n-1)%mod;
		else ans+=1LL*getC(n,i)*getC(m-i*k-1,n-1)%mod;
		if(ans<mod) ans+=mod;
		if(ans>=mod) ans-=mod;
	}
	printf("%d\n",ans);
	return 0;
}

例題三、P1450 [HAOI2008]硬幣購物

題目描述

傳送門

分析

同樣的方法,我們可以預處理出沒有硬幣個數限制的方案數,即完全揹包
再通過容斥原理求出不滿足限制的方案數

程式碼

#include<cstdio>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
typedef long long ll;
const int maxn=1e5+5;
ll f[maxn],ans;
int c[maxn],d[maxn],n,s;
int main(){
	c[1]=read(),c[2]=read(),c[3]=read(),c[4]=read();
	n=read();
	f[0]=1;
	for(rg int i=1;i<=4;i++){
		for(rg int j=c[i];j<maxn;j++){
			f[j]+=f[j-c[i]];
		}
	}
	int now=0,nans=0;
	for(rg int i=1;i<=n;i++){
		ans=0;
		d[1]=read(),d[2]=read(),d[3]=read(),d[4]=read(),s=read();
		for(rg int j=1;j<=15;j++){
			now=0,nans=0;
			for(rg int k=1;k<=4;k++){
				if(j&(1<<(k-1))){
					now++;
					nans+=(d[k]+1)*c[k];
				}
			}
			if(s>=nans){
				if(now&1) ans+=f[s-nans];
				else ans-=f[s-nans];
			}
		}
		ans=f[s]-ans;
		printf("%lld\n",ans);
	}
	return 0;
}

二、Min-max 容斥

公式

對於全序集合\(S\),有

\[\begin{split} \max S &= \sum_{T\subseteq S}(-1)^{|T|-1} \min T\\ \min S &= \sum_{T\subseteq S}(-1)^{|T|-1} \max T \end{split} \]


以上引用自OI-WIKI

證明

對於第一個式子
我們設 \(A_k\)​ 為 \(U\) 內元素降序排序後排名第 \(k\) 的元素,也就是第 \(k\) 大。
\(k=1\) ,那麼 \(A_1\) 作為最小值只會出現一次 ,係數為\(1\)
\(k>1\),那麼 \(A_k\) 作為最小值會出現 \(2^{k-1}\) 次,其中有 \(2^{k-2}\) 次出現在元素個數為偶數的序列中,另外的\(2^{k-2}\)次出現在元素個數為奇數的序列中
最終加和的結果即為 \(A_1\)
對於第二個式子也是同理
這個東西主要用在期望題中,因為期望下的 \(\min\)\(\max\)是很難求的

例題、禮物

題目描述

夏川的生日就要到了。作為夏川形式上的男朋友,季堂打算給夏川買一些生日禮物。商店裡一共有種禮物。夏川每得到一種禮物,就會獲得相應喜悅值 \(W_i\)​(每種禮物的喜悅值不能重複獲得)。每次,店員會按照一定的概率 \(P_i\)​(或者不拿出禮物),將第 \(i\)種禮物拿出來。季堂每次都會將店員拿出來的禮物買下來。沒有拿出來視為什麼都沒有買到,也 算一次購買。

眾所周知,白毛切開都是黑的。所以季堂希望最後夏川的喜悅值儘可能地高。

求夏川最後最大的喜悅值是多少,並求出使夏川得到這個喜悅值,季堂的期望購買次數。

輸入格式

第一行,一個整數 \(N\),表示有 \(N\) 種禮物。

接下來 \(N\)行,每行一個實數 \(P_i\)​和正整數 \(W_i\)​,表示第 \(i\) 種禮物被拿出來的概率和可以獲得喜悅值。

輸出格式

第一行,一個整數表示可以獲得的最大喜悅值。

第二行,一個實數表示獲得這個喜悅值的期望購買次數,保留 \(3\) 位小數。

樣例

樣例輸入

3
0.1 2
0.2 5
0.3 7

樣例輸出

14
12.167

資料範圍與提示

對於 \(10\%\)的資料, \(N=1\)

對於 \(30\%\) 的資料, \(N\le5\)

對於 \(100\%\)的資料, \(N \le 25 ,0 < Wi \le 10^9 ,0 < Pi \le 1\text{且}\sum P_i \le 1\)

分析

顯然第一問應該輸出所有禮物的喜悅值之和
對於第二問,用\(max\)代表將集合裡的禮物全部買齊的期望,用 \(min\)代表買到集合中禮物任意一件的期望
套一下式子就可以了

程式碼

#include<cstdio>
const int maxn=30;
int a[maxn],n;
double p[maxn],ans2;
long long ans1;
void dfs(int now,double sum,int cnt){
	if(now>n){
		if(cnt&1) ans2+=1.0/sum;
		else if(cnt) ans2-=1.0/sum;
		return;
	}
	dfs(now+1,sum+p[now],cnt+1);
	dfs(now+1,sum,cnt);
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%lf%d",&p[i],&a[i]);
		ans1=ans1+a[i];
	}
	printf("%lld\n",ans1);
	dfs(1,0,0);
	printf("%.3f\n",ans2);
	return 0;
}