【题解】组合数学

来自\(\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

上一篇:【题解】CF229D Towers


下一篇:PHP开发套件