容斥原理學習筆記
獲得更好的閱讀體驗,請開啟夜間模式
定義
在計數時,必須注意沒有重複,沒有遺漏。為了使重疊部分不被重複計算,人們研究出一種新的計數方法,這種方法的基本思想是:先不考慮重疊的情況,把包含於某內容中的所有物件的數目先計算出來,然後再把計數時重複計算的數目排斥出去,使得計算的結果既無遺漏又無重複,這種計數的方法稱為容斥原理。
一、普通容斥
公式
設 \(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;
}