组合计数

痛恨一切形式的数数题。

  1. A. 矩形选点 (2000)

考虑点对 \((x_1,y_1)\)\((x_2,y_2)\) ,先计算它们的 \(x\) 的贡献,假设 \(x_2-x_1 = d\) ,那么 \(x_1\) 可以放在 \([1,n-d]\) 行,总共 \(n-d\) 种情况。

而除了行数,还需要考虑列数,发现两个点所在的列是有序的,所以共有 \(m^2\) 种选择,而贡献为 \(d\) ,所以只考虑两个点的总贡献为 \(d \times (n-d) \times m^2\)

除了这两个点,其他的点都是可以随便放的,所以只考虑 \(x\) 的总答案为 \(d \times (n-d) \times m^2 \times C_{nm-2}^{k-2}\)

对于 \(y\) 同理,加和即可。

故可知最后的答案为 \((\sum_{d=1}^{n-1}d \times (n-d) \times m^2+\sum_{d=1}^{m-1}d \times (m-d) \times n^2)\times C_{nm-2}^{k-2}\)

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int mod=1e9+7;
const int N=2e5+5;
ll n,m,k;
ll fac[N],inv[N];
ll qpow(ll a,ll b){
	ll res=1;
	while(b){
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}
void pre(int n){
	fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
	inv[n]=qpow(fac[n],mod-2);
	for(int i=n-1;i>=0;i--) inv[i]=inv[i+1]*(i+1)%mod;
}
ll C(int n,int m){ return fac[n]*inv[m]%mod*inv[n-m]%mod; }
ll ans;
int main(){
	scanf("%lld%lld%lld",&n,&m,&k);
	pre(n*m);
	for(int d=1;d<n;d++) ans=(ans+d*(n-d)%mod*m%mod*m%mod)%mod;
	for(int d=1;d<m;d++) ans=(ans+d*(m-d)%mod*n%mod*n%mod)%mod;
	ans=ans*C(n*m-2,k-2)%mod;
	printf("%lld",ans);
	return 0;
}
  1. NOIP2021 数列

先打一个暴力,容易想到根据 \(a_i\) 从小到大放,判断可行性,将答案乘上方案数算入总贡献。

亲测可得 \(20pts\)

#include <bits/stdc++.h>
using namespace std;
const int N=35;
const int mod=998244353;
int n,m,k;
int v[N],a[N],c[N];
int C(int n,int m){
	int c=1;
	for(int i=1;i<=n;i++) c*=i;
	for(int i=1;i<=m;i++) c/=i;
	for(int i=1;i<=n-m;i++) c/=i;
	return c;
}
int ans;
void dfs(int x){
	if(x==n+1){
		int res=0,tmp=1,p=0;
		for(int i=1;i<=n;i++) res+=(1ll<<a[i]);
		if(__builtin_popcount(res)>k) return;
		res=1;
		for(int i=1;i<=n;i++) c[a[i]]++,res=1ll*res*v[a[i]]%mod;;
		for(int i=0;i<=m;i++) tmp=1ll*tmp*C(n-p,c[i])%mod,p+=c[i];
		for(int i=0;i<=m;i++) c[i]=0;
		ans=(ans+1ll*res*tmp%mod)%mod;
		return;
	}
	for(int i=a[x-1];i<=m;i++){
		a[x]=i;
		dfs(x+1);
	} 
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i=0;i<=m;i++) scanf("%d",&v[i]);
	dfs(1);
	printf("%d",ans);
	return 0;
}

\(dp(i,j,k,l)\) 表示 \(a\) 序列已经放了 \(i\) 个数,其中进位到的最高一位为 \(j\),也就是说填到了 \(j-1\) 位(注意到这里的进位不能引起连锁反应,即进位到的最高一位上的数有可能大于 \(1\)),除了最高位的其他数位的 \(1\) 的个数为 \(k\) ,最高位有 \(l\)\(1\) 等待进位时的贡献总和。

考虑用刷表法转移,假设现在有一个长度为 \(t\) 的连续段都填了 \(j\)

所以 \(dp(i,j,k,l)\) 能转移到 \(dp(i+t,j+1,k+(t+l)\mod 2,\lfloor \frac{t+l}{2} \rfloor)\)

这些数能贡献 \(v_{j}^t\times C_{n-i}^{t} \times dp(i,j,k,l)\) ,累加即可。

注意数组别开小了。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=35;
const int mod=998244353;
ll dp[N][105][N][N];
ll v[105],pw[105][N];
int n,m,K;
void add(ll &x,ll y){ x=(x+y)%mod; }
ll C[N][N],ans;
void pre(){
	for(int i=0;i<=n;i++) C[i][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
int main(){
	scanf("%d%d%d",&n,&m,&K);
	for(int i=0;i<=m;i++){
		scanf("%lld",&v[i]);
		pw[i][0]=1;
		for(int j=1;j<=n;j++) pw[i][j]=pw[i][j-1]*v[i]%mod;
	}
	pre();
	dp[0][0][0][0]=1;
	for(int i=0;i<=n;i++){
		for(int j=0;j<=m;j++){
			for(int k=0;k<=K;k++){
				for(int l=0;l<=n/2;l++){
					if(!dp[i][j][k][l]) continue;
					for(int t=0;i+t<=n;t++) add(dp[i+t][j+1][k+(t+l)%2][(t+l)>>1],dp[i][j][k][l]*pw[j][t]%mod*C[n-i][t]%mod);
				}
			}
		}
	}
	for(int k=0;k<=K;k++)
		for(int l=0;l<=n/2;l++)
			if(k+__builtin_popcount(l)<=K) add(ans,dp[n][m+1][k][l]);
	printf("%lld",ans);
	return 0;
}
  1. CF140E New Year Garland

upd:看懂了,我爱 dp 计数。

首先,只有一行是可做的。

\(f_{i,j}\) 表示填到第 \(i\) 个位置时用了 \(j\) 种颜色的方案数,\(j\) 种颜色是一个升序排列

\(f_{i,j}=f_{i-1,j} \times (j-1) + f_{i-1,j-1}\) ,意义表示为这个点可以选前面出现过的颜色,但不能和前一个位置是同一种颜色,共有 \(j-1\) 种选择方案,当然也可以新开一个颜色,这里没有乘任何数的原因在于我定义颜色必须为一个升序排列。

这就不得不提到这个题,题解区有两种写法,第一种是乘了,第二种是没乘。是 dp 的定义导致了这样的问题,具体来说第一种写法定义的就是前 \(i\) 个球放在 \(j\) 个盒子里的方案数,盒子是无序的,也就是在讨论当新增一个盒子的时候,我可以把这个盒子随机插到前面的 \(j\) 个空中。第二种写法则定义为前 \(i\) 个球放在 \(j\) 个从小到大的盒子中的方案数,盒子是有序的,所以不能插空,只能放在尾部。

由于盒子不能为空,所以在第二种写法的最后会乘上一个排列,两种写法殊途同归,但对于本题,第二种写法在行与行之间转移更为方便。

所以一行的总方案就为 \(\sum_{j=1}^{min(l_i,m)}f_{l_i,j} \times A_{m}^{j}\)

于是再考虑多行,但不管颜色集合是否会发生冲突。

\(dp_{i,j}\) 表示前 \(i\) 行,第 \(i\) 行选了 \(j\) 种颜色的方案数,需要注意的一点是,这里的 \(j\) 种颜色升序但不一定是排列

所以 \(dp_{i.j}=\sum_{k=1}^{min(l_{i-1},m)} dp_{i-1,k} \times (f_{l_i,j} \times C_{m}^{j})\)

那如果集合颜色不能相同呢,需要在这一行的方案中减掉第 \(i\) 行和第 \(i-1\) 行颜色集合相同的情况,应该有 \(dp_{i,j}=\sum_{k=1}^{min(l_{i-1},m)} dp_{i-1,k} \times (f_{l_i,j} \times C_{m}^{j})-dp_{i-1,j} \times f_{l_i,j}\)

这里我关于 \(dp_{i-1,j} \times f_{l_i,j}\) 为什么不用乘 \(C_{m}^{j}\) 想了好久,原因在于如果乘了,就减多了,意义就变了。

具体来说,看一组数据,假设有两行,以下是是第一行 \(4\) 种颜色选 \(3\) 种的情况。

1 2 3
1 2 4
1 3 4
2 3 4

如果乘了 \(C_{m}^{j}\),即减掉了 \(dp_{i-1,j} \times f_{l_i,j} \times C_{m}^{j}\),那么进行到第二行时,就相当于把 1 2 3 看作和1 2 31 2 41 3 42 3 4颜色集合相同了。

这显然是不合法的,所以减多了。

你以为这就结束了?这只是升序但不一定是排列的情况,但这道题也不一定是升序啊。

其实这个很好处理,我只需要改一下 \(dp\) 数组的定义,定义为表示前 \(i\) 行,第 \(i\) 行选了 \(j\) 种颜色,颜色无序的总方案数。

那只需要在转移的时候乘一个 \(j!\) 即可,也就是说得到了最终的状态转移方程为:

\[dp_{i,j}=j!\times(\sum_{k=1}^{min(l_{i-1},m)} dp_{i-1,k} \times (f_{l_i,j} \times C_{m}^{j})-dp_{i-1,j} \times f_{l_i,j}) \]

\[=\sum_{k=1}^{min(l_{i-1},m)} dp_{i-1,k} \times (f_{l_i,j} \times A_{m}^{j})-dp_{i-1,j} \times f_{l_i,j} \times j! \]

最后答案为 \(\sum_{j=1}^{min(l_n,m)} dp_{n,j}\)

预处理一下即可。

滚动数组优化空间时别忘了清空上一次的数组。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
const int N=1e6+5,M=5e3+5;
int n,m,mod,mx;
ll fac[N],A[N];
ll f[M][M],dp[2][M];
void add(ll &x,ll y){ x=(x+y)%mod; }
int l[N];
void pre(){
	fac[0]=1;
	for(int i=1;i<=m;i++) fac[i]=fac[i-1]*i%mod;
	A[0]=1;
	for(int i=1;i<=m;i++) A[i]=A[i-1]*(m-i+1)%mod;
	f[0][0]=1;
	for(int i=1;i<=mx;i++)
		for(int j=1;j<=min(i,m);j++)
			add(f[i][j],(f[i-1][j]*(j-1)+f[i-1][j-1])%mod);
}
ll ans;
int main(){
	scanf("%d%d%d",&n,&m,&mod);
	for(int i=1;i<=n;i++) scanf("%d",&l[i]),mx=max(mx,l[i]);
	pre();
	int pre=0,cur=1;
	dp[cur][0]=1;
	for(int i=1;i<=n;i++){
		ll sum=0;
		swap(pre,cur);
		for(int j=0;j<=min(l[i-1],m);j++) add(sum,dp[pre][j]);
		for(int j=0;j<=min(l[i],m);j++){
			if(j>l[i-1]) dp[pre][j]=0;
			ll tmp=sum*f[l[i]][j]%mod*A[j]%mod-dp[pre][j]*f[l[i]][j]%mod*fac[j]%mod;
			tmp=(tmp+mod)%mod;
			add(dp[cur][j]=0,tmp);
		}
	}
	for(int i=0;i<=min(l[n],m);i++) add(ans,dp[cur][i]);
	printf("%lld",ans);
	return 0;
}
  1. ABC134F Permutation Oddness

会了,十分感谢这篇题解

实在是太牛逼了。

#include <bits/stdc++.h>
using namespace std;
const int N=105;
const int mod=1e9+7;
int n,m;
int dp[N][N][3005];
void add(int &x,int y){ x=(x+y)%mod; }
int main(){
	scanf("%d%d",&n,&m);
	dp[1][0][0]=dp[1][1][2]=1;
	for(int i=1;i<n;i++){
		for(int j=0;j<=i;j++){
			for(int k=0;k<=m;k++){
				if(!dp[i][j][k]) continue;
				add(dp[i+1][j][k+2*j],dp[i][j][k]);
				if(j){
					add(dp[i+1][j][k+2*j],2ll*j*dp[i][j][k]%mod);
					add(dp[i+1][j-1][k+2*(j-1)],1ll*j*j*dp[i][j][k]%mod);
				}
				add(dp[i+1][j+1][k+2*(j+1)],dp[i][j][k]);
			}
		}
	}
	printf("%d",dp[n][0][m]);
	return 0;
}
posted @ 2023-09-13 08:46  syta  阅读(80)  评论(1)    收藏  举报