努力更新 x 2(
简介
DP 套 DP 顾名思义,就是在一层 DP(或者自动机)外面再套一层,把里面的 DP(或自动机)的结果作为外层 DP 的状态,从而更方便地转移。
这么说好像很抽象,我也不知道自己在说什么,还是看题目吧 qwq
例题:[TJOI2018] 游园会
题目链接:https://www.luogu.com.cn/problem/P4590
题目要算的字符串总共有三种限制:长度为 $N$,不出现子串 NOI,以及最难搞的和奖章串的最长公共子序列长为 $i$。而且对于每一个 $i$ 都要算一次,如果直接算复杂度就爆炸了。
考虑 DP,状态 $f_{i,j,k}$ 表示现在构造到第 $i$ 位,与兑奖串的最长公共子序列长为 $j$,当前后缀出现了 NOI 的前 $k$ 位时的兑奖串个数,是一个计数 DP。这样 DP 看起来很对,但是有一个地方有问题:维护 $j$ 的时候,没有维护当前扫到了奖章串的哪一位,导致我们不知道现在构造的第 $i$ 位应该和奖章串的哪一个字符比较。于是状态应该是 $f_{i,(g_1,g_2,\cdots,g_K),k}$,其中 $i$ 和 $k$ 的含义同上,$K$ 是奖章串长度,中间的 $(g_1,g_2,\cdots,g_K)$ 代表当前扫到奖章串的第 $1,2,\cdots,K$ 位时,最长公共子序列长是多少。在转移的时候,枚举第 $i$ 位填什么,$g$ 数组就是一个单独的 DP,也就是内层 DP。而 $f$ 数组的转移就是外层 DP。
但是这样的状态根本存不下。观察一下 $g$ 数组有什么性质,发现它的差分数组全是 $0$ 和 $1$,且 $K$ 很小,可以状压。这样状态数就变成了 $O(NK2^K)$。转移的时候直接枚举 $i$、状压后的差分数组、 $k$ 和接下来填的字母,时间复杂度 $O(NK2^K)$。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <iostream> #include <cstdio> using namespace std;
const int mod = 1e9 + 7;
int n,K; char s[1010]; int a[1010]; int g[20],gg[20]; int f[2][33000][3],ans[30];
int main() { scanf("%d%d",&n,&K); scanf("%s",s + 1); for (int i = 1;i <= K;i ++) a[i] = (s[i] == 'N' ? 0 : s[i] == 'O' ? 1 : 2); f[0][0][0] = 1; int p = 1; for (int i = 1;i <= n;i ++,p ^= 1) { for (int j = 0;j < (1 << K);j ++) for (int k = 0;k <= 2;k ++) f[p][j][k] = 0; for (int j = 0;j < (1 << K);j ++) { for (int k = 1;k <= K;k ++) g[k] = g[k - 1] + (j >> (k - 1) & 1); for (int k = 0;k <= 2;k ++) { for (int w = 1;w <= K;w ++) gg[w] = max(max(gg[w - 1],g[w]),g[w - 1] + (k == a[w])); int tmp = 0; for (int w = K;w >= 1;w --) tmp = (tmp << 1 | (gg[w] - gg[w - 1])); for (int nw = 0;nw <= 2;nw ++) { if (nw == 2 && k == 2) break; (f[p][tmp][(k == nw ? nw + 1 : (!k ? 1 : 0))] += f[p ^ 1][j][nw]) %= mod; } } } } p ^= 1; for (int i = 0;i < (1 << K);i ++) { for (int j = 1;j <= K;j ++) g[j] = g[j - 1] + (i >> (j - 1) & 1); (ans[g[K]] += ((f[p][i][0] + f[p][i][1]) % mod + f[p][i][2]) % mod) %= mod; } for (int i = 0;i <= K;i ++) printf("%d\n",ans[i]); }
|
可以发现,DP 套 DP 这个名字确实很形象。
其他题目
[ZJOI2019] 麻将
题目链接:https://www.luogu.com.cn/problem/P5279
这道题的内层 DP 是个自动机 ,我还没有搞懂,先放在这里(
[SDOI/SXOI2022] 小 N 的独立集
题目链接:https://www.luogu.com.cn/problem/P8352
像上面的例题一样,可以搞一个计数 DP,$f_{i,j,k}$ 代表以 $i$ 为根的子树中,选 $i$ 和不选 $i$ 的答案分别是 $j$ 和 $k$ 的数量。状态和转移的时间复杂度还是一如既往地爆炸。观察一下 $j$ 和 $k$ 之间的关系,发现如果 $j \le k$,那 $j$ 没有一点用;否则 $j - k \le K$(这里的 $K$ 就是原题面的 $k$),不然的话可以通过从选 $i$ 的最优方案中去掉 $i$ 创造出一种新的、比当前最优的不选 $i$ 的方案更优的方案,产生矛盾。这样就可以换一种状态表示,$f_{i,j,k}$ 表示以 $i$ 为根的子树中,不选 $i$ 和选不选 $i$ 都行(也就是上面 $j$ 和 $k$ 的较大值)的答案分别是 $j$ 和 $k$。由于 $k \ge j$ 且 $0\le k - j \le K$,可以把状态写成 $f_{i,j,k - j}$,状态数变成 $O(n^2K^2)$。根据树上背包的时间复杂度分析,总的时间复杂度是 $O(n^2K^4)$,因为还要枚举每个点填什么。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| #include <iostream> #include <cstdio> using namespace std;
const int mod = 1e9 + 7;
int n,K; int to[2010],nxt[2010],head[1010],cnt; int siz[1010],f[1010][5010][7],ff[1010][5010][7]; int ans[5010];
void add(int x,int y) { to[++ cnt] = y; nxt[cnt] = head[x]; head[x] = cnt; }
void dfs(int x,int la) { siz[x] = 1; for (int i = 1;i <= K;i ++) f[x][0][i] = 1; for (int i = head[x];i;i = nxt[i]) { int u = to[i]; if (u == la) continue; dfs(u,x); for (int j = 0;j <= siz[x] * K;j ++) for (int k = 0;k <= K;k ++) { if (!f[x][j][k]) continue; int valx1 = j,valx2 = j + k; for (int l = 0;l <= siz[u] * K;l ++) for (int w = 0;w <= K;w ++) { if (!f[u][l][w]) continue; int valu1 = l,valu2 = l + w; int newval1 = valx1 + valu2,newval2 = valx2 + valu1; newval2 = max(newval2,newval1); (ff[x][newval1][newval2 - newval1] += 1ll * f[x][j][k] * f[u][l][w] % mod) %= mod; } } siz[x] += siz[u]; for (int j = 0;j <= siz[x] * K;j ++) for (int k = 0;k <= K;k ++) f[x][j][k] = ff[x][j][k],ff[x][j][k] = 0; } }
int main() { scanf("%d%d",&n,&K); for (int i = 1;i < n;i ++) { int x,y; scanf("%d%d",&x,&y); add(x,y),add(y,x); } dfs(1,0); for (int i = 0;i <= n * K;i ++) for (int j = 0;j <= K;j ++) ans[i + j] = (ans[i + j] + f[1][i][j]) % mod; for (int i = 1;i <= n * K;i ++) printf("%d\n",ans[i]); }
|
(DP 套 DP 的代码好像还挺好写的?直接按照算法流程写就行了)
其他题目待补 qwq