第 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\),求方案数。
这个问题的答案是:
这个 \(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;
}

浙公网安备 33010602011771号