Re:从爆零开始的预设型DP考题——DP搬运工之我们不生产DP,我们只是DP的搬运工

我们只是DP的搬运工

题面

题面在这里

有的没的

预设型dp的意思大概是 枚举当前放哪个数。
搬运工系列都是计数dp。


DP搬运工1

dp当然要从设立dp数组开始。

定义状态 \(dp[i][j][k]\) 表示 填到位置 \(i\) 、还有 \(j\) 个位置可以填数、贡献总和为 \(k\) 的方案数。

假定从小到大填数,对于一个数 \(x\) ,分类讨论:

  1. 如果 \(x\) 的左右在之后的操作中都被填上了数,那它左右的数都比它大,所以当前的 \(x\) 不一定会产生贡献。

  2. 如果之后的操作仅在 \(x\) 的一边填上了数,那 \(x\) 仍比它另一边的数大,所以贡献是 \(x\)

  3. 如果之后的操作没有在 \(x\) 的任意一边填上数, \(x\) 仍比它两边都大,贡献为 \(2x\)

同理,来推新加入的数:

  1. 如果新填入的点在数列的两端:

    若空出相邻的一个位置,能填数的位置 \(j + 1\)

    \(max\) 的总和 \(k\) 不会改变。

    左右两个端点,乘法原理,有 :$$dp[i][j + 1][k] += dp[i - 1][j][k] \times 2$$

    如果相邻位置没有空出, \(i\) 比它相邻的数大,贡献 \(k + i\)

    同乘法原理,有:$$dp[i][j][k + i] += dp[i - 1][j][k] \times 2$$

  2. 如果新填入的数在数列的中间:

    当插入的数两边拓展出两个位置,但插入数占了一个位置,剩下的位置 \(j + 1\)\(i\) 会比它之后相邻的数小,不会产生贡献。

    又有 \(j\) 个位置可以填数,可得:$$dp[i][j + 1][k] += dp[i - 1][j][k] \times j$$

    当插入的数只扩展出一个位置,剩下的位置不变,贡献 \(k + i\)

    \(j\) 个位置填数,每个位置有相邻两个填好了的数,所以有 \(2 \times j\) 种选择,有:$$dp[i][j][k + i] += dp[i - 1][j][k] \times j \times 2$$

    当插入的数没有扩展出位置, \(j - 1\) ,同时,贡献 \(k + i + i\)

    \(j\) 个位置,有:$$dp[i][j - 1][k + i + i] += dp[i - 1][j][k] \times j$$

最后,答案就是 $$\sum_{i = 0}^{m} dp[n][0][i]$$

Code

#include<cstdio>
#include<algorithm>

#define LL long long

using namespace std;

const int Mod = 998244353;
const int MAXN = 55, MAXK = 2510;
int n, m;
LL ans;
LL dp[MAXN][MAXN][MAXK];
//dp[i][j][k] 表示填到位置 i ,还有 j 个位置可以填数,相邻两个数的 max 的和为 k 的方案数

int main(){
    scanf("%d%d", &n, &m);

    dp[1][0][0] = 1;
    for(register int i = 2; i <= n; i++){
        for(register int j = 0; j <= n - i + 1; j++){
            for(register int k = 0; k <= m; k++){
                if(dp[i - 1][j][k] != 0){ //上一状态可得到 
                    dp[i][j + 1][k] = (dp[i][j + 1][k] + dp[i - 1][j][k] * 2) % Mod; //在端点空出一个位置 
                    dp[i][j][k + i] = (dp[i][j][k + i] + dp[i - 1][j][k] * 2) % Mod; //在端点没有空出位置 

                    if(j != 0){ //上一状态没有空位,那此状态就无法实现 
                        dp[i][j + 1][k] = (dp[i][j + 1][k] + dp[i - 1][j][k] * j) % Mod;
                        dp[i][j][k + i] = (dp[i][j][k + i] + dp[i - 1][j][k] * j * 2) % Mod;
                        dp[i][j - 1][k + i + i] = (dp[i][j - 1][k + i + i] + dp[i - 1][j][k] * j) % Mod;
                    }
                }
            }
        }
    }

    for(register int i = 0; i <= m; i++)
        ans = (ans + dp[n][0][i]) % Mod;

    printf("%lld", ans);

    return 0;
}

DP搬运工2

这应该是最短最简单都不简单)的一道了。

定义状态:\(dp[i][j]\) 表示表示 填到数 \(i\)\(j\) 个数满足要求的方案数。

仍然默认从小往大插数。

对于 \(dp[i - 1][j]\) 我们向序列中插入一个数 \(i\) 会有以下情况:

  1. \(i\) 插入到原来满足要求的 \(j\) 个数旁边, \(i\) 大于它旁边的数,之前的数不再满足它两边都小于它,所以 \(j\) 不变。

    \(j\) 个数,它的两边都可以插,所以有 \(j \times 2\) 个位置。

    同时,插入数列的两端也是同样的效果。\(a_{1}\)\(a_{n}\) 不会产生贡献,插入后原 \(a_{1}\) 变成了 \(a_{2}\) ,此时 \(a_{2} < a_{1}\) 仍不产生贡献。 \(a_{n}\) 同理。

    所以,一共有 \(j \times 2 + 2 = (j + 1) \times 2\) 种方案,可得:$$dp[i][j] = dp[i][j - 1] \times (j + 1) \times 2$$

  2. \(i\) 插入到其他位置,此时 \(i\) 大于它两边的数,满足的数 \(j + 1\)

    结合上边情况,整个数列一共有 \(i\) 个位置,插入其他位置就有 \(i - (j + 1) \times 2\) 种方案,可得:$$dp[i][j + 1] = dp[i - 1][j] \times (i - (j + 1) \times 2) $$

最后的答案就是 \(dp[n][k]\)

Code

#include<cstdio>
#include<algorithm>

using namespace std;

const int MAXN = 2010;
const int Mod = 998244353;
int n, k;
long long dp[MAXN][MAXN];
//dp[i][j] 表示 1 ~ i 位置上有 j 个位置满足要求的方案数 
int main(){
    scanf("%d%d", &n, &k);

    dp[1][0] = 1;
    dp[2][0] = 2;
    for(register int i = 3; i <= n; i++){
        for(register int j = 0; j <= k; j++){
            dp[i][j] = (dp[i][j] + dp[i - 1][j] * (j + 1) * 2) % Mod;
            dp[i][j + 1] = (dp[i][j + 1] + dp[i - 1][j] * (i - (j + 1) * 2)) % Mod;
        }
    }

    printf("%lld", dp[n][k]);

    return 0;
}

DP搬运工3

定义状态:\(dp[i][j][k]\) 表示填到了数字 \(i\)\(i\) 之前有 \(j\) 个位置没有填,当前产生的价值为 \(k\)

首先先转化一下,可以固定排列 \(B\) ,因为一个排列 \(B\) 只会对应一个排列 \(A\)

排列 \(B\) 的方案数有 \(n!\) 种,所以只要最后对答案乘一个 \(n!\) 就行了。

不妨设排列 \(B\)\(1\)\(n\)

然后分类讨论:

  1. 位置 \(i\) 不填数:

    贡献不变,空出的位置数 \(j + 1\) 。从 \(i - 1\) 位置直接转移。$$dp[i][j + 1][k] += dp[i - 1][j][k]$$

  2. \(i\) 插入到之前的 \(j\) 个位置中的一个:

    插入了该位置,空的位置数 \(j --\) ,但位置 \(i\) 也空出来了, \(j++\) ,所以, \(j\) 不变。

    因为 \(B_{j} = j\) ,并且 \(i > j\) ,所以 \(i\)会产生贡献,贡献改变为 \(k + i\)

    再者一共有 \(j\) 个选点,可乘法原理得。$$dp[i][j][k + i] += dp[i - 1][j][k] \times j$$

  3. 从之前没用的 \(j\) 中选一个插到位置 \(i\) :

    \(i\) 向前跳了 \(1\)\(j++\) 。但位置 \(i\) 又被填上了, \(j\) 不变。

    同理 \(B_{i} = i\)\(A_{i} = j\)\(i > j\) ,所以 \(i\) 会产生贡献,贡献改变为 \(k + i\)

    同样,有 \(j\) 个数待选,乘法原理得。$$dp[i][j][k + i] += dp[i - 1][j][k] \times j$$

  4. \(i\) 填到位置 \(i\)

    \(j\) 肯定不变。

    \(A_{i} = B_{i}\)\(i\) 会产生贡献,贡献改变为 \(k + i\)

    方案仍直接转移。$$dp[i][j][k + i] += dp[i - 1][j][k] \times j$$

  5. 既把 \(i\) 插入到之前的 \(j\) 个位置,也从 \(j\) 中选一个数填到位置 \(i\)

    结合上边说的情况2,3,可知 \(j\) 仍然不变。

    两个 \(i\) 都产生贡献,贡献改变为 \(k + i + i\)

    同样,乘法原理。$$dp[i][j - 1][k + i + i] += dp[i - 1][j][k] \times j \times j$$

当然,情况2到4可以合并。

\[dp[i][j][k + i] += dp[i - 1][j][k] \times (2 \times j + 1) \]

最后的答案就是

\[\sum_{i = k}^{n \times n} dp[n][0][i] \]

Code

#include<cstdio>
#include<algorithm>

#define LL long long

using namespace std;

const int MAXN = 55;
const int Mod = 998244353;
int n, m;
LL dp[MAXN][MAXN][MAXN * MAXN];
//dp[i][j][k] 表示填到了数字 i, i 之前有 j 个位置没有填, 当前产生的价值为 k 
LL ans;

int main(){
    scanf("%d%d", &n, &m);

    dp[0][0][0] = 1;
    dp[1][0][1] = 1;
    dp[1][1][0] = 1;
    for(register int i = 2; i <= n; i++){
        for(register int j = 0; j <= i - 1; j++){ //到i最多有i - 1个位置没有填数
            for(register int k = 0; k <= i * i; k++){ //贡献最大的话是i * i
                if(!dp[i - 1][j][k]) continue; //如果为0的话肯定是没法达成这种情况,没法向后续转移 
                dp[i][j + 1][k] = (dp[i][j + 1][k] + dp[i - 1][j][k]) % Mod;
                dp[i][j][k + i] = (dp[i][j][k + i] + dp[i - 1][j][k] * j) % Mod;
                dp[i][j][k + i] = (dp[i][j][k + i] + dp[i - 1][j][k] * j) % Mod;
                dp[i][j][k + i] = (dp[i][j][k + i] + dp[i - 1][j][k]) % Mod;

                if(!j) continue;
                dp[i][j - 1][k + i + i] = (dp[i][j - 1][k + i + i] + dp[i - 1][j][k] * j * j) % Mod; 
            }
        }
    }

    for(register int i = m; i <= n * n; i++)
        ans = (ans + dp[n][0][i]) % Mod;
    for(register int i = 2; i <= n; i++)
        ans = 1LL * ans * i % Mod;

    printf("%lld", ans);

    return 0;
}
posted @ 2022-08-03 21:38  TSTYFST  阅读(187)  评论(10编辑  收藏  举报