CF1581D - Mathematics Curriculum(动态规划)

题目

长度为\(n\)的排列中,对于一个数\(x\),如果所有包含\(x\)的区间的最大值恰有\(m\)个不同的值,那么\(x\)是好的。问有多少长度为\(n\)的排列,给定\(m\),恰好有\(k\)个好的的数?\(n\le 100\)

题解

感觉不好直接计数,试试动态规划。设\(f[n][m][k]\)代表满足条件的排列数。可以发现一个很好的性质,每加入一个新的数\(n+1\),它会分别使得其左右部分中数的不同最大值个数加1。枚举插入\(n+1\)的位置,容易得到转移方程:

\[f[n][m][k]=\sum\limits_{t=0}^{n-1}{C(n-1,t)\sum_{a=0}^{k}{f[t][m-1][a] \times f[n-1-t][m-1][k-a]}} \]

这样多状态的转移方程最麻烦的就是边界条件的设置!要严格按照\(f\)的含义设置边界条件。容易发现,最外层枚举\(m\)比较好,因为转移都是从\(m-1\)\(m\)。那么要设置的边界值就是\(f[n][1][k]\)

如何设置边界值?对于\(f[n][1][k]\),会发现比较难确定\(k=0\)时的值。这是因为不知道\(f[n][m][0]\)的值是多少。这时,假如把\(f\)的定义改一下,改成有大于等于\(k\)个好的的数,令其为\(g\)那么显然有

\[g[n][m][k]=\sum_{i=k}^{\infty}{f[n][m][i]} \\ g[n][m][0]=n! \]

因为大于等于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。那么就有边界情况:

\[\begin{aligned} f[n][1][1]&=n!\\ f[0][1][0]&=1 \end{aligned} \]

其余情况都为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;
}
posted @ 2021-10-08 17:35  limil  阅读(51)  评论(0编辑  收藏  举报