Re:从爆零开始的预设型DP考题——DP搬运工之我们不生产DP,我们只是DP的搬运工
我们只是DP的搬运工
题面
题面在这里
有的没的
预设型dp的意思大概是 枚举当前放哪个数。
搬运工系列都是计数dp。
DP搬运工1
dp当然要从设立dp数组开始。
定义状态 \(dp[i][j][k]\) 表示 填到位置 \(i\) 、还有 \(j\) 个位置可以填数、贡献总和为 \(k\) 的方案数。
假定从小到大填数,对于一个数 \(x\) ,分类讨论:
-
如果 \(x\) 的左右在之后的操作中都被填上了数,那它左右的数都比它大,所以当前的 \(x\) 不一定会产生贡献。
-
如果之后的操作仅在 \(x\) 的一边填上了数,那 \(x\) 仍比它另一边的数大,所以贡献是 \(x\) 。
-
如果之后的操作没有在 \(x\) 的任意一边填上数, \(x\) 仍比它两边都大,贡献为 \(2x\) 。
同理,来推新加入的数:
-
如果新填入的点在数列的两端:
若空出相邻的一个位置,能填数的位置 \(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$$
-
如果新填入的数在数列的中间:
当插入的数两边拓展出两个位置,但插入数占了一个位置,剩下的位置 \(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\) 会有以下情况:
-
\(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$$
-
\(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\) 。
然后分类讨论:
-
位置 \(i\) 不填数:
贡献不变,空出的位置数 \(j + 1\) 。从 \(i - 1\) 位置直接转移。$$dp[i][j + 1][k] += dp[i - 1][j][k]$$
-
把 \(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$$
-
从之前没用的 \(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$$
-
把 \(i\) 填到位置 \(i\) :
这 \(j\) 肯定不变。
则 \(A_{i} = B_{i}\),\(i\) 会产生贡献,贡献改变为 \(k + i\) 。
方案仍直接转移。$$dp[i][j][k + i] += dp[i - 1][j][k] \times j$$
-
既把 \(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可以合并。
最后的答案就是
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;
}
以下为博客签名,与博文无关。
只要你们不停下来,那前面就一定有我。所以啊,不要停下来~
本文来自博客园,作者:TSTYFST,转载请注明原文链接:https://www.cnblogs.com/TSTYFST/p/16515258.html