第 14 届陕西省国际大学生程序设计竞赛 D题思路分享(dp,组合计数)

题意概述

你和 \(A\) 轮流报数,每次选一个 \(1\)\(2n\) 还没被选过的数报出。你先手,\(A\) 只会选所有没选过的数中最小的。如果 \(A\) 报出的数的总和 \(\ge K\),那么 \(A\) 就胜利。求所有让 \(A\) 胜利的方案数,模 \(p\)

\(1\le n \le 100\)

思路

考虑 \(dp\),这类 \(dp\) 问题一般都先考虑如何在已有方案的基础上,计算有多少选法。

假设已经有了你选的数的集合 \(a\)\(A\) 选的数的集合 \(b\)

\(a,b\) 都升序排序,因为 \(A\) 的报数顺序一定递增,问题转化成如何给每个 \(a_i\) 分配一个报数顺序 \(p_i\)

\(i\)\(A\) 报的数为 \(b_i\),需要所有属于集合 \(a\),小于 \(b_i\) 的数都已经被报出,即所有 \(a_j \lt b_i\)\(p_j \le i\)

可以发现,每个 \(p_i\) 都有一个上界,这个上界就是第一个满足 \(b_i \gt a_i\)\(i\),记为 \(mx_i\)

这样就变成了一个经典问题:给定一个数组,把数组每个元素填到一个新数组中,需要满足第 \(i\) 个元素填的位置 \(\le mx_i\),求方案数。

这个问题的答案是:

\[\prod_{i}^{n}{\min(n,mx_i)-i+1} \]

这个 \(mx_i\) 可以用双指针很容易的求出,但在后续 \(dp\) 的过程中却很难处理,因此考虑寻找一种更方便处理(依赖状态量更少)的形式。

\(mx_i\) 的本质就是 \(b_i\) 中比 \(a_i\) 小的元素数量 \(+1\)。对于 \(a_i\),一共有 \(a_i-1\) 个元素比它小,\(a\) 中有 \(i-1\) 个元素比它小,那么 \(b_i\) 中就有 \(a_i-1-(i-1) = a_i-i\) 个,于是 \(mx_i = a_i-i+1\)

这样就优美多了,\(mx_i\) 只依赖 \(a_i\)\(i\)

\(dp[i][j][k]\) 为枚举到第 \(i\) 位,自己选了 \(j\) 个数,\(A\) 选的数的总和为 \(k\)

枚举 \(2n\) 个数,讨论第 \(i\) 个数选给 \(A\) 还是自己,如果选给自己乘一下贡献。要用滚动数组优化一维空间。

时间复杂度 \(\mathcal{O}(n^4)\)

代码

//author:kzssCCC

#include <bits/stdc++.h>
using namespace std;
using ll = long long;


void solve(){
	int n,K,MOD;
	cin >> n >> K >> MOD;

	vector<vector<ll>> dp(n+1,vector<ll>(n*(n+1)+1));
	dp[0][0] = 1;

	for (int i=1;i<=2*n;i++){
		vector<vector<ll>> ndp(n+1,vector<ll>(n*(n+1)+1));
		
		for (int j=0;j<=n;j++){
			for (int k=0;k<=n*(n+1);k++){
				if (dp[j][k]==0) continue;
				
				if (i-j<=n && k+i<=n*(n+1)){
					ndp[j][k+i] = (ndp[j][k+i]+dp[j][k])%MOD;
				} 	
				if (j+1<=n){
					ndp[j+1][k] = (ndp[j+1][k]+dp[j][k]*(min(n,i-j)-j)%MOD)%MOD;				
				}
			}
		}

		dp = ndp;
	}

	ll res = 0;
	for (int k=K;k<=n*(n+1);k++){
		res = (res+dp[n][k])%MOD;
	}

	cout << res << '\n';
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int t = 1;
	// cin >> t;
	while (t--) solve();

	return 0;
}
posted @ 2026-05-18 16:27  kzssCCC  阅读(23)  评论(0)    收藏  举报