CF1581D - Mathematics Curriculum(动态规划)
题目
长度为\(n\)的排列中,对于一个数\(x\),如果所有包含\(x\)的区间的最大值恰有\(m\)个不同的值,那么\(x\)是好的。问有多少长度为\(n\)的排列,给定\(m\),恰好有\(k\)个好的的数?\(n\le 100\)
题解
感觉不好直接计数,试试动态规划。设\(f[n][m][k]\)代表满足条件的排列数。可以发现一个很好的性质,每加入一个新的数\(n+1\),它会分别使得其左右部分中数的不同最大值个数加1。枚举插入\(n+1\)的位置,容易得到转移方程:
这样多状态的转移方程最麻烦的就是边界条件的设置!要严格按照\(f\)的含义设置边界条件。容易发现,最外层枚举\(m\)比较好,因为转移都是从\(m-1\)到\(m\)。那么要设置的边界值就是\(f[n][1][k]\)。
如何设置边界值?对于\(f[n][1][k]\),会发现比较难确定\(k=0\)时的值。这是因为不知道\(f[n][m][0]\)的值是多少。这时,假如把\(f\)的定义改一下,改成有大于等于\(k\)个好的的数,令其为\(g\)那么显然有
因为大于等于0个就是所有排列的情况。那么\(k\)恰好等于0的情况就是就是\(f[n][m][0] = g[n][m][0]-g[n][m][1]\)。那么有\(g[n][1][0]=n!(n\ge 0)\),\(g[n][1][1]=n!(n>0)\),其余情况都是0。那么就有边界情况:
其余情况都为0,这样就有边界值\(f[n][1][k]\)啦。直接套\(dp\)方程,轻松秒杀(doge)。
时间复杂度\(O(n^2mk^2)\)?是不是有点太慢了?确实慢,因此需要剪枝。因为有\(m\le n\),\(k \le n-m+1\)的限制,可以减少一点计算。即使如此,2s能跑这个复杂度,还是觉得有点神奇,然而事实就是这样(官方题解也差不多是这个复杂度),小编也感到很疑惑。
细节:
- 注意在使用限制减少计算量时,\(n\)和\(k\)取到0时是例外,不能被省略了。比如要记得预处理\(f[n][m][0]\)的值。
- 0是十分特殊的位置,要注意。它的结果是\(所有情况-非0情况\)。不要漏了0处的值,也不要想当然觉得它的值就是0。
- 取模真的很耗时,在里面的循环使用
long long
,直接累加不取模,然后到外面循环再取模。速度提升效果显著。
#include <bits/stdc++.h>
#define endl '\n'
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mp make_pair
#define seteps(N) fixed << setprecision(N)
typedef long long ll;
using namespace std;
/*-----------------------------------------------------------------*/
ll gcd(ll a, ll b) {return b ? gcd(b, a % b) : a;}
#define INF 0x3f3f3f3f
const int N = 110;
const double eps = 1e-5;
int C[N][N];
int dp[N][N][N];
int fact[N];
inline int cal(int m, int n, int k, int p) {
ll res = 0;
for(int t = 0; t <= n - 1; t++) {
ll tres = 1ll * dp[m - 1][t][0] * dp[m - 1][n - 1 - t][k] % p;
if(k) tres <<= 1;
if(t >= m - 1 && t <= n - m) {
for(int a = max(1, k - n + t + m - 1); a <= min(k - 1, t - m + 2); a++) {
tres = tres + 1ll * dp[m - 1][t][a] * dp[m - 1][n - 1 - t][k - a] % p;
}
}
tres %= p;
res = (res + C[n - 1][t] * tres % p);
}
return res % p;
}
int main() {
IOS;
int n, m, k, p;
cin >> n >> m >> k >> p;
fact[0] = 1;
for(int i = 1; i <= n; i++) {
fact[i] = 1ll * fact[i - 1] * i % p;
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; j++) {
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % p;
}
}
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= n; j++) {
dp[j][i][0] = fact[i];
}
}
for(int i = 1; i <= n; i++) {
dp[1][i][1] = fact[i];
dp[1][i][0] = 0;
}
for(int i = 2; i <= m; i++) {
for(int j = i; j <= n; j++) {
ll sum = 0;
for(int k = 1; k <= j - i + 1; k++) {
dp[i][j][k] = cal(i, j, k, p);
sum += dp[i][j][k];
}
dp[i][j][0] -= sum % p;
dp[i][j][0] = (dp[i][j][0] + p) % p;
}
}
cout << dp[m][n][k] << endl;
}