1. 程式人生 > 其它 >【題解】組合數學

【題解】組合數學

來自\(\texttt{SharpnessV}\)省選複習計劃中的組合數學


由於作者非常菜所以只能隨便寫點基礎的。


P3197 [HNOI2008]越獄

簡單數數。越獄的方案數等於總方案數減沒有越獄的方案數。

所以\(Ans=m^n-m\times (m-1)^{n-1}\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
#define P 100003
using namespace std;
typedef long long ll;
ll Pow(ll x,ll y){
	ll now=1;
	for(;y;y>>=1,x=x*x%P)if(y&1)now=now*x%P;
	return now;
}
int main(){
	ll m,n;scanf("%lld%lld",&m,&n);
	printf("%lld\n",(Pow(m,n)-m*Pow(m-1,n-1)%P+P)%P);
	return 0;
}

下面是組合數環節,以下是必背恆等式。

遞推式

\[\binom{n}{m}=\binom{n-1}{m-1}+\binom {n-1}{m} \]

階乘式

\[\binom{n}{m}=\dfrac{n!}{m!(n-m)!} \]

對稱式

\[\binom{n}{m}=\binom{n}{n-m} \]

合併式

\[\binom{n}{k}\binom{k}{m}=\binom{n}{m}\binom{n-m}{k-m} \]

求和式 \(1\)

\[\sum\limits_{i=0}^n\binom{m+i}{i}=\binom{n+m+1}{n} \]

求和式 \(2\)

\[\sum\limits_{i=m}^n\binom{i}{m}=\binom{n+1}{m+1} \]

二項式定理

\[(a+b)^n=\sum\limits_{i=0}^n \binom{n}{i}a^ib^{n-i} \]

範德蒙德卷積

\[\sum\limits_{i=0}^{k}\binom{n}{i}\binom{m}{k-i}=\binom{n+m}{k} \]

奇偶和相等

\[\sum\limits_{i=2k}\binom{n}{i}=\sum\limits_{i=2k+1}\binom{n}{i} \]

除了等式,還需要知道二項式反演

\[f(n)=\sum\limits_{i=0}^{n}\binom{n}{i}g(i)\ \ \iff\ \ g(n)=\sum\limits_{i=0}^{n}(-1)^{n-i}\binom{n}{i}f(i) \]

盧卡斯定理

\[\binom{n}{m}\equiv \binom{\left\lfloor\frac{n}{p}\right\rfloor}{\left\lfloor\frac{m}{p}\right\rfloor}\binom{n\bmod p}{m\bmod p} \pmod{p} \]

P2822 [NOIP2016 提高組] 組合數問題

直接遞推求組合數。時間複雜度\(\rm O(N^2)\),好處是不用擔心 \(P\) 不是質數。

#include<bits/stdc++.h>
using namespace std;
int t,k;
int f[2005][2005],sum[2005][2005];
void prepare(){
	f[0][0]=1;
	for(int i=1;i<=2000;i++){
	  for(int j=1;j<=2000;j++)
	    f[i][j]=(f[i-1][j-1]+f[i-1][j])%k;
	  f[i][0]=1;
	}
	for(int i=0;i<=2000;i++)
	  for(int j=0;j<=2000;j++)
	    if(i>=j&&!f[i][j])
	      sum[i][j]++;
	for(int i=1;i<=2000;i++){
	  for(int j=1;j<=2000;j++)
	    sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
	}
}
int main()
{
	scanf("%d%d",&t,&k);
	prepare();
	while(t--){
		int x,y;scanf("%d%d",&x,&y);
		printf("%d\n",sum[x][y]);
	}
	return 0;
}

P2290 [HNOI2004]樹的計數

\(\rm Prufer\) 序列,唯一需要知道的是一個長度為 \(n-2\) 的值域為 \(n\) 的序列與一顆有標號無根樹對應,並且是一一對應關係。

並且度數為 \(d_i\) 的節點在序列中恰好出現 \(d_i-1\) 次。

所以 \(n\) 個節點的無根樹個數是 \(n^{n-2}\)

這道題給定度數,就是可重集排列,直接套公式即可。

Code


P3807 【模板】盧卡斯定理

套用定理,預處理\(p\)以內的階乘,對於\(n,m\ge p\)的組合數,利用定理遞迴下去即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
using namespace std;
int n,m,P,fac[N],inv[N];
int Pow(int x,int y){
	int now=1;
	for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
	return now;
}
int C(int n,int m){
	if(n<m||n<0||m<0)return 0;
	return 1LL*fac[n]*inv[m]*inv[n-m]%P;
}
int lucas(int n,int m){
	if(n<P&&m<P)return C(n,m);
	return 1LL*lucas(n/P,m/P)*lucas(n%P,m%P)%P;
}
int main(){
	int T;scanf("%d",&T);
	fac[0]=inv[0]=1;
	while(T--){
		scanf("%d%d%d",&n,&m,&P);
		rep(i,1,P-1)fac[i]=1LL*fac[i-1]*i%P,inv[i]=Pow(fac[i],P-2);
		printf("%d\n",lucas(n+m,n));
	}
	return 0;
}

P7044 「MCOI-03」括號

非常水的基礎組合數學題。

首先我們對每個括號算貢獻,簡單容易一下得到一個括號的貢獻為可重集組合。

這裡的可重集組合是每個數都有無限個,從 \(n\) 種數種選擇 \(m\) 個數的方案數。

我們假設第 \(i\) 次選的數是 \(a_i\),不失一般性我們令 \(a_i\) 不減。

\(k_i=a_i+i-1\) ,不難發現 \(k_i\) 遞增,且 \(k_i\) 序列與 \(a_i\) 序列一一對應。

\(k_i\) 的取值範圍是 \(n+m-1\),所以可重集組合數就是\(\binom{n+m-1}{m}\)

#include<bits/stdc++.h>
#define int long long 
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 2000005
#define P 998244353
using namespace std;
typedef long long ll;
char s[N];int n,k,sta[N],top;ll fac[N<<1],cur[N],inv[N<<1],ans;
ll Pow(ll x,int y){ll now=1;for(;y;y>>=1,x=x*x%P)if(y&1)now=now*x%P;return now;}
ll C(int x,int y){return fac[x]*inv[y]%P*inv[x-y]%P;}
void init(){
	inv[0]=fac[0]=1;
	rep(i,1,n+k)fac[i]=fac[i-1]*i%P,inv[i]=Pow(fac[i],P-2);
	rep(i,1,n)cur[i]=C(i+k-1,k);
}
ll R(int x,int y){return (cur[x]-cur[x-y])%P;}
ll T(int x){return cur[x];}
signed main(){
	scanf("%lld%lld",&n,&k);
	scanf("%s",s+1),init();
	for(int i=1;i<=n;i++)if(s[i]=='(')sta[++top]=i;
		else ans+=1LL*R(i,i-sta[top])*T(n-i+1)%P,top=std::max(0LL,top-1);
	top=0;sta[0]=n+1;
	for(int i=n;i;i--)if(s[i]==')')sta[++top]=i;
		else ans+=T(i)*R(n-i+1,sta[top]-i)%P,top=std::max(top-1,0LL);
	printf("%lld\n",(ans%P+P)%P);
	return 0;
}

P4071 [SDOI2016]排列計數

錯位排列問題,\(D_n\) 表示 \(n\) 個數的錯排方案數。

我們對最後一個數 \(n\) ,它可以錯開到 \(1\sim n-1\)

假設它錯開到 \(i\) ,那麼如果第 \(n\) 位為 \(i\) ,方案數為\(D_{n-2}\),否則方案數為 \(D_{n-1}\)

這樣我們就得到了遞推式\(D_n=(n-1)(D_{n-1}+D_{n-2})\),或者換一種寫法\(D_n=nD_{n-1}+(-1)^n\)

Code


P2480 [SDOI2010]古代豬文

給定\(n,g\),求 \(\large g^{\sum\limits_{d\mid n}\binom{n}{d}}\bmod 999911659\)

\(999911659\)是一個質數,根據費馬小定理,我們直接求\(\sum\limits_{d\mid n}\binom{n}{d}\bmod 999911658\) 即可。

這個數顯然不是質數,但是這個數可不是亂寫的,\(999911658=2\times 3\times 4679\times 35617\),這又是四個小質數。

我們對每個質數用\(\rm Lucas\)求出\(\binom{n}{d}\),然後用 \(\rm CRT\) 合併答案即可。

Code


P4769 [NOI2018] 氣泡排序

首先存在長度為 $3 $ 的下降子序列的序列不是好的。

可以簡單理解一下,如果存在,則中間的元素必定要向右一次後向左一次,等於做了無用功,無法卡到下界。

那麼我們要求不存在長度為 \(3\) 的下降子序列。

我們不難\(\rm DP\),設 \(f[i][j]\) 表示前 \(i\) 個數 ,最大數為 \(j\) 的方案數。如果\(a_{i+1}>j\),則\(f[i][j]\to f[i+1][a_{i+1}]\)。否則,\(a_{i+1}\)必須為最小的數,有\(f[i][j]\to f[i+1][j]\)

我們把 \(f[i][j]\) 看成平面直角座標系的點 \((i,j)\),我們從\((0,0)\)開始,要到\((n,n)\),每次可以直接向右移動一步,也可以向右上移動。

向右上移動看起來非常沒有規律,改一下,將向右上移動等價於先向上移動\(k\)步,再向右移動一步。

這就很清晰了,我們從原點開始,到\((n,n)\)結束,每次可以選擇向右或向上移動一步的方案數。

還有一個現實條件\(i\le j\),因為排列中 \(i\) 個數的最大值一定不小於 \(i\),在平面上,就是路徑穿過 \(y=x\) 的直線。

這就是經典組合模型,直接折線法,\(\rm Ans=\dbinom{2n}{n}-\dbinom{2n}{n-1}\)

考慮字典序的限制條件,類似於數位\(\rm DP\),我們前求出前\(i\)個走到的點,並計算當前點到終點的方案數。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1200006
#define P 998244353
using namespace std;
int Pow(int x,int y){
	int now=1;
	for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
	return now;
}
int fac[N],inv[N],n;
int C(int x,int y){
	if(x<y||y<0||x<0)return 0;
	return 1LL*fac[x]*inv[y]%P*inv[x-y]%P;
}
int get(int x,int y){return (C(n-x+n-y,n-x)-C(n-x+n-y,n-y-1))%P;}
int a[N],c[N];
inline void add(int x){for(;x<=n;x+=x&-x)c[x]++;}
inline int ask(int x){int sum=0;for(;x;x-=x&-x)sum+=c[x];return sum;}
void solve(){
	memset(c,0,sizeof(c));
	scanf("%d",&n);
	rep(i,1,n)scanf("%d",&a[i]);
	int y=0,ans=0;
	rep(i,1,n-1){
		if(a[i]<y){
			ans=(ans+get(i-1,y+1))%P;
			if(ask(a[i])!=a[i]-1)break;
		}
		else ans=(ans+get(i-1,a[i]+1))%P;
		y=max(a[i],y);add(a[i]);
	}
	printf("%d\n",(ans+P)%P);
}
int main(){
	fac[0]=inv[0]=1;
	rep(i,1,N-5)fac[i]=1LL*fac[i-1]*i%P,inv[i]=Pow(fac[i],P-2);
	int T;scanf("%d",&T);
	while(T--)solve();
	return 0;
}

P4931 [MtOI2018]情侶?給我燒了!(加強版)

不難想到列舉匹配的情侶和他們坐的位置,有\(\dbinom{n}{k}^2k!\) 的方案數。

考慮剩下的情侶,錯排問題,定義 \(D_i\)\(i\) 對情侶的錯排方案,不考慮排列順序。

經典錯排問題中\(D_i=(i-1)(D_{i-1}+D_{i-2})\)

這裡錯排的情侶可以男女配對,也可以同行配對,所以方案數翻倍。

\[D_i=2(i-1)(D_{i-1}+D_{i-2}) \]

還考慮每對座椅分左右,再\(\times 2^n\)

總的方案數為\(\dbinom{n}{k}^2k!2^n(n-k)!D_{n-k}\),直接遞推即可。

這裡要線性複雜度,所以預處理階乘,次冪,併線性求逆元。

這樣我們不用生成函式解決了這道題。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5000005
#define P 998244353
using namespace std;
int w = N - 5 , inv[N] , fac[N] , d[N] , pw[N];
int C(int n,int m){
	if(n<0||m<0||n<m)return 0;
	return 1LL*fac[n]*inv[m]%P*inv[n-m]%P;
}
int main(){
	pw[0]=1;
	rep(i,1,w)pw[i]=(pw[i-1]<<1)%P;
	inv[1]=1;rep(i,2,w)inv[i]=-1LL*(P/i)*inv[P%i]%P;
	fac[0]=inv[0]=1;
	rep(i,1,w)fac[i]=1LL*fac[i-1]*i%P,inv[i]=1LL*inv[i-1]*inv[i]%P;
	d[0]=1;d[1]=0;
	rep(i,2,w)d[i]=1LL*2*(i-1)*(d[i-1]+d[i-2])%P;
	int T;scanf("%d",&T);
	while(T--){
		int n,k;scanf("%d%d",&n,&k);
		printf("%lld\n",1LL*C(n,k)*C(n,k)%P*fac[k]%P*pw[n]%P*d[(n-k)]%P*fac[n-k]%P);
	}
	return 0;
}

P3726 [AH2017/HNOI2017]拋硬幣

難度很高的一題。

首先考慮 \(a=b\) 的情況。

對於任意一種 \(A\) 贏了 \(B\) 的情況,將每次拋硬幣正負取反,對應於一種 \(B\) 贏了 \(A\) 的情況。

那麼我們將所有情況分為平局和不平局的情況,\(A\)贏了的情況顯然是不平局的情況的\(\dfrac{1}{2}\)

所以我們只用計算平局的情況,列舉正面向上的次數,得到\(\sum\limits_{i=0}^a \dbinom{n}{i}^2\),卷積一下得到\(\dbinom{2n}{n}\)

考慮 \(a>b\) 的情況。

首先對於平局\(x=y\)的情況,\(a-x>b-y\),取反後一定是 \(A\) 贏 。

對於 \(A\) 輸局 \(x<y\) 的情況,\(a-x>b-y\),取反後也一定是 \(A\) 贏。

對於 \(A\) 贏局 \(x>y\) 的情況,無法判斷 \(a-x\)\(b-y\) 的大小。

我們發現前兩種情況都是輸贏對應,最後一種可能輸贏對應,也可能都是贏。

我們計算都是贏的情況,有\(a-x>b-y\to a-b>x-y\) ,考慮到\(a-b\le 10^4\),我們可以列舉 \(y\)\(x-y\),令 \(k=x-y\)

\[\begin{aligned} sum&=\sum\limits_{k=1}^{a-b}\sum\limits_{y=0}^b\binom{a}{y+k}\binom{b}{y}\\&=\sum\limits_{k=1}^{a-b}\sum\limits_{y=0}^b\binom{a}{y+k}\binom{b}{b-y}\\&=\sum\limits_{k=1}^{a-b}\binom{a+b}{k+b}\end{aligned} \]

然後直接計算組合數即可,模數不為質數,考慮擴充套件\(\rm Lucas\)


P1655 小朋友的球\

斯特林數。

遞推式

\[\begin{Bmatrix}n\\m\end{Bmatrix}=\begin{Bmatrix}n-1\\m-1\end{Bmatrix}+m\begin{Bmatrix}n-1\\m\end{Bmatrix} \]

組合意義,考慮第 \(n\) 個元素重新分組還是加入原先的組。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 105
long long s[N][N];
int main(){
	s[1][1]=1;
	rep(i,2,N-5)rep(j,1,i)s[i][j]=s[i-1][j]*j+s[i-1][j-1];
	int n,m;
	while(~scanf("%d%d",&n,&m))printf("%lld\n",s[n][m]);
	return 0;
}
//這道題需要高精度

P5395 第二類斯特林數·行

可能有用的模板。

考慮將 \(n\) 個球放入 \(m\) 個不同盒子中,允許有空盒子,得到恆等式。

\[m^n=\sum\limits_{i=0}^{m}\binom{m}{i}\begin{Bmatrix}n\\i\end{Bmatrix}i! \]

\(f(x)=x^n,g(x)=\begin{Bmatrix}n\\x\end{Bmatrix}x!\),發現上面就是個二項式反演的標準形式。一波推導後的到

\[\begin{Bmatrix}n\\i\end{Bmatrix}=\sum\limits_{i=0}^{m}\dfrac{(-1)^{m-i}i^n}{(m-i)!i!} \]

發現這就是一個卷積,直接\(\rm NTT\)即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 600005
#define P 167772161
using namespace std;
int Pow(int x,int y){
	if(y<0)return Pow(Pow(x,P-2),-y);
	int now=1;
	for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
	return now;
}
int n,a[N],b[N],rev[N],t,fac[N];
void ntt(int *u,int op){
	rep(i,0,t-1)if(rev[i]>i)swap(u[rev[i]],u[i]);
	for(int l=1,k=2;k<=t;k<<=1,l<<=1){
		int cur=Pow(3,(P-1)/k*op);
		for(int i=0;i<t;i+=k){
			int now=1;
			rep(j,0,l-1){
				int x=u[i+j],y=1LL*u[i+j+l]*now%P;
				u[i+j]=(x+y)%P;u[i+j+l]=(x-y)%P;
				now=1LL*now*cur%P;
			}
		}
	}
}
int main(){
	scanf("%d",&n);
	t=1;while(t<=n*2)t<<=1;
	rep(i,1,t-1)rev[i]=(rev[i>>1]>>1)|((i&1)?(t>>1):0);
	fac[0]=1;rep(i,1,n)fac[i]=1LL*fac[i-1]*i%P;
	rep(i,0,n)a[i]=1LL*Pow(i,n)*Pow(fac[i],P-2)%P,b[i]=Pow(-1,i)*Pow(fac[i],P-2);
	ntt(a,1);ntt(b,1);rep(i,0,t-1)a[i]=1LL*a[i]*b[i]%P;ntt(a,-1);
	int cur=Pow(t,-1);
	rep(i,0,n)printf("%lld ",(1LL*cur*a[i]%P+P)%P);
	return 0;
} 

P6620 [省選聯考 2020 A 卷] 組合數問題

看起來多項式乘次冪乘組合數非常不可做。

一個比較討論的思路是通常冪轉下降冪。

眾所周知

\[x^n=\sum\limits_{i=0}^{n}\begin{Bmatrix}n\\i\end{Bmatrix}x^{\underline{i}} \]

這裡多項式直接轉下降冪

\[\begin{aligned}Ans&=\sum\limits_{k=0}^{n}\sum\limits_{i=0}^mb_ik^{\underline{i}}\binom{n}{k}x^k\\&=\sum\limits_{k=0}^{n}\sum\limits_{i=0}^mb_in^{\underline{i}}\binom{n-i}{k-i}x^k\\&=\sum\limits_{i=0}^mb_in^{\underline{i}}x^i(x+1)^{n-i}\end{aligned} \]

這裡已經可以\(\rm O(M\log M)\)計算,我們還需要計算下降冪的係數\(b_i\),直接斯特林數即可。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1005
int P,n,m,x,a[N],b[N],s[N][N];
int Pow(int x,int y){
	int now=1;
	for(;y;y>>=1,x=1LL*x*x%P)if(y&1)now=1LL*now*x%P;
	return now;
} 
int main(){
	scanf("%d%d%d%d",&n,&x,&P,&m);
	rep(i,0,m)scanf("%d",&a[i]);
	s[0][0]=1;
	rep(i,1,m)rep(j,1,m)s[i][j]=(s[i-1][j-1]+1LL*s[i-1][j]*j)%P;
	rep(i,0,m)rep(j,i,m)b[i]=(b[i]+1LL*a[j]*s[j][i])%P;
	int ans=0;
	rep(i,0,m){
		int cur=1;
		rep(j,n-i+1,n)cur=1LL*cur*j%P;
		ans=(ans+1LL*b[i]*cur%P*Pow(x,i)%P*Pow(x+1,n-i)%P)%P;
	}
	printf("%d\n",ans);
	return 0;
} 

P2532 [AHOI2012]樹屋階梯

卡特蘭數。

我們定義 \(f[i]\) 表示 \(i\) 級階梯的方案數,我們列舉矩形將階梯分成兩部分,得到兩個遞迴子問題,從而得到方程\(f[i]=\sum\limits_{j=0}^{i-1}f[j]\times f[i-j-1]\)

這就是卡特蘭數的遞推式,一般卡特蘭數寫作\(C_i\)

\[C_n=\sum \limits_{i=0}^{n-1}C_iC_{n-i-1} \]

卡特蘭數有許多經典模型:

  • 給定\(1\sim n\)的入棧數列,合法的出棧序列數
  • \(n\)邊形的三角形劃分
  • \(n\)個節點的二叉樹個數
  • \(n\)對括號的合法括號序列數

這些我們都可以直接用遞推式序列,比如括號匹配,我們定義\(f_i\)表示\(n\)對括號的方案數。顯然第一個一定是左括號,我們列舉與之匹配的右括號位置,這就是卡特蘭數的遞推式。

我們可以換一個思路\(\rm DP\),定義\(f[i][j]\)表示長度為 \(i\) 的序列,剩餘 \(j\) 個左括號的方案數,顯然 \(j\ge 0\)

在平面直角座標系中,就是從原點出發,每次可以向右上或右下走一格,走到\((2n,0)\)的方案數,不能碰到直線 \(y=-1\)

直接用折線法可以得到方案數,即為卡特蘭數, \(C_n=\dbinom{2n}{n}-\dbinom{2n}{n-1}\)

把組合數拆開可以得到 \(C_n=\dfrac{(2n)!}{n!(n+1)!}\)

同時可以得到一階遞推公式 \(C_n=\dfrac{4n-2}{n+1}C_{n-1}\)

Code


P3200 [HNOI2009]有趣的數列

從小到大考慮每個數的填放位置。

不難發現對於奇數位和偶數位,都是從左往右填,且任何時刻奇數位填的數不少於偶數位填的數。

我們定義\(f[i][j]\)表示奇數位填了 \(i\) 個,偶數位填了 \(j\) 個。顯然\(f[i][j]\)可以轉移到\(f[i+1][j]\)\(f[i][j+1]\),且不能穿過直線 \(y=x\)

這就是卡特蘭數的折現模型,答案為\(C_n\)

Code


P3978 [TJOI2015]概率論

首先 \(n\) 個節點的二叉樹有 \(C_n\)個。

我們將二叉樹的葉子刪去一個,得到 \(k\) 個大小為 \(n-1\) 的二叉樹。

反過來,我們可以在大小為 \(n-1\) 的二叉樹上掛一個節點,得到大小為 \(n\) 的二叉樹,每個二叉樹有 \(n\) 個可以掛的位置。

所以葉子數之和為 \(nC_{n-1}\),答案為\(\dfrac{nC_{n-1}}{C_n}=\dfrac{n(n+1)}{2(2n-1)}\)

#include<bits/stdc++.h>
int main(){
	int n;scanf("%d",&n);
	return printf("%.10lf",1.00*n*(n+1)/2/(2*n-1)),0;
}

容斥原理。

有若干個物品,每個物品有若干屬性(可能沒有),現在要求出帶有屬性的物品個數。

我們加上至少帶有一個屬性的物品,減去至少帶有兩個屬性的物品,再加上至少帶有三個,以此類推。

而最簡單的容斥就是求補集,有時候答案集合並不好求,我們直接求答案集合的補集。

P5664 [CSP-S2019] Emiya 家今天的飯

簡單容斥,考慮總方案數減去不符合條件的。

不符合條件就是某個食材用了超過\(\left\lfloor\dfrac{k}{2}\right\rfloor\)次,最多隻有一個食材超過了。我們列舉是哪一個食材超過了,然後減去即可。

其實如果限制是\(\left\lfloor\dfrac{k}{3}\right\rfloor\),甚至是\(\left\lfloor\dfrac{k}{4}\right\rfloor\)都是可以做的,不妨思考一下。

Code


P1450 [HAOI2008]硬幣購物

硬幣種類很少,考慮容斥。

先不考慮數量限制,直接多重揹包求出方案。

然後列舉哪些硬幣超過了限制,容斥即可得到答案。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 100005
using namespace std;
int c[4],d[4],s,n,w=N-5;long long f[N];
int main(){
	rep(i,0,3)scanf("%d",&c[i]);
	f[0]=1;
	rep(i,0,3)rep(j,0,w-c[i])f[j+c[i]]+=f[j];
	scanf("%d",&n);
	rep(i,1,n){
		rep(j,0,3)scanf("%d",&d[j]);
		scanf("%d",&s);long long ans=f[s];
		rep(S,1,15){
			long long cur=0,op=1;
			rep(k,0,3)if((S>>k)&1)op*=-1,cur+=(d[k]+1)*c[k];
			if(cur<=s)ans+=op*f[s-cur];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

P4336 [SHOI2016]黑暗前的幻想鄉

\(n-1\) 個公司每個公司修恰好一條邊的生成樹個數。

如果不考慮什麼公司修,直接矩陣樹定理即可。

既然要考慮,直接容斥即可。把哪些公司沒有修作為容斥中物品的屬性,那麼答案就是總方案數減去有屬性的物品數量,經典容斥模型。

Code