【学习笔记】DP套DP
前置知识:dp 是啥你总得知道吧()
dp 套 dp,又叫 dp of dp,此类题目一般以一个 dp 问题为方案,求方案数。比如求最长上升子序列长度为 \(x\) 的方案数。
为了避免混淆,以下称内 dp 的数组为 \(f\),外 dp 为 \(F\)。
BZOJ3864 Hero meet devil
(想不到吧 bzoj 登陆洛谷了)
那就以这个经典问题为例。
(一定要会 \(\mathcal{O}(n^2)\) 求 LCS!)
题意
给定一个字符集为 ACGT 的字符串 \(S\)。定义 \(\text{LCS}(S,T)\) 为两个字符串 \(S,T\) 的最长公共子序列。
对于每个 \(0\leq i \leq |S|\),求有多少个长度为 \(m\),字符集 ACGT 的字符串 \(T\),满足 \(|\text{LCS}(S,T)|=i\),答案对 \(10^9+7\) 取模。
\(1\leq T\leq 5\),\(1\leq |S| \leq 15\),\(1\leq m\leq 1000\)。
思路
dp of dp 的原理是,把内 dp 一些信息存入外 dp 数组下标,然后在外 dp 转移的时候,枚举下标,然后将下标(即内 dp 的信息)输入一个函数,返回出内 dp 的转移值作为外 dp 转移的下标。
就本题来说,先想想求 LCS 的过程是怎样的——枚举 \(T\) 中的 \(i\)、\(S\) 中的 \(j\),再转移。那是不是一个这样的东西:给你现在 \(T_i\) 是多少,你已经有了 \(\{f_{i-1,j}\}\) 的数组,然后你再遍历一遍 \(S\),求出 \(\{f_{i,j}\}\) 的数组。假设有一个函数叫做 \(trans\),那刚刚的过程就可以形象地表示如下:
这就是外 dp 的过程,需要枚举的下标是 \(i\) 和 \(\{f_{i-1,j}\}\),而 \(trans\) 实际上是内 dp。
设 \(F_{i,mask}\) 表示,\(T\) 中遍历到 \(i\),\(\{f_{i,j}\}\) 的状压为 \(mask\)。
但是 \(\{f_{i,j}\}\) 数组怎么状压?总不能按进制存吧。
观察一下,不难发现 \(f_{i,1}\sim f_{i,|S|}\) 值单调不降,并且前后两项最多差 \(1\)。所以可以把它差分一下,变成一个二进制 \(mask\)!
这样一来,代码框架就基本出来了:
外部枚举 \(i,T_i\),转移是
内部枚举 \(j\),转移是
最后统计答案就看 mask 里有几个 1 说明 LCS 多长。
如果内部每次都 dp 一遍,时间复杂度为 \(\mathcal{O}(m \times |字符集| \times 2^{|S|} \times |S|)\),有点悬啊!
注意到对于相同的参数 \(T_i\) 和 \(mask\),返回的 \(trans(T_i,mask)\) 一样。所以把所有的情况先预处理一遍,求 \(F\) 的时候就不用每次算了。
时间复杂度 \(\mathcal{O}(|字符集|\times 2^{|S|}\times |S| + m \times |字符集| \times 2^{|S|})\),毫无压力!
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1005, M = 20, MASK = (2 << 15) + 5, MOD = 1e9 + 7;
char char_set[] = {"ACGT"};
vector<int> Mask[20];
int n, m, f[2][M], trans[MASK][4], F[2][MASK], ans[M];
char s[M];
inline int madd(int x, int y) { return x + y >= MOD ? x + y - MOD : x + y; }
inline int mmul(int x, int y) { return x * y % MOD; }
inline void add(int &to, int from)
{
to = madd(to, from);
}
int DP(int mask, char c)
{
memset(f, 0, sizeof(f));
for (int i = 1; i <= m; i++)
f[0][i] = f[0][i - 1] + ((mask >> (i - 1)) & 1);
for (int i = 1; i <= m; i++)
{
f[1][i] = max(f[0][i], f[1][i - 1]);
if (c == s[i])
f[1][i] = max(f[1][i], f[0][i - 1] + 1);
}
int res = 0;
for (int i = 1; i <= m; i++)
res += (f[1][i] - f[1][i - 1]) << (i - 1);
return res;
}
void solve()
{
scanf("%s%d", s + 1, &n);
m = strlen(s + 1);
for (int mask = 0; mask < (1 << m); mask++)
for (int k = 0; k < 4; k++)
trans[mask][k] = DP(mask, char_set[k]);
int cur = 0, lst = 1;
memset(F, 0, sizeof(F));
F[cur][0] = 1;
for (int i = 1; i <= n; i++)
{
swap(cur, lst);
memset(F[cur], 0, sizeof(F[cur]));
for (int k = 0; k < 4; k++)
for (int mask = 0; mask < (1 << m); mask++)
add(F[cur][trans[mask][k]], F[lst][mask]);
}
memset(ans, 0, sizeof(ans));
for (int mask = 0; mask < (1 << m); mask++)
add(ans[__builtin_popcount(mask)], F[cur][mask]);
for (int i = 0; i <= m; i++)
printf("%d\n", ans[i]);
}
int main()
{
int T;
cin >> T;
while (T--)
solve();
return 0;
}
[TJOI2018] 游园会
一个很像的题,不过生成串中不能出现 NOI 罢了。\(F\) 数组再记录一维表示 \(3\) 个阶段即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1005, M = 20, MASK = (1 << 15) + 5, MOD = 1e9 + 7;
char cset[] = {"NOI"};
int n, m, f[2][M], trans[MASK][3], F[2][4][MASK], ans[M];
char s[M];
inline int madd(int x, int y) { return x + y >= MOD ? x + y - MOD : x + y; }
inline void add(int &to, int fro)
{
to = madd(to, fro);
}
int DP(int mask, char c)
{
memset(f, 0, sizeof(f));
for (int i = 1; i <= m; i++)
f[0][i] = f[0][i - 1] + ((mask >> (i - 1)) & 1);
for (int i = 1; i <= m; i++)
{
f[1][i] = max(f[0][i], f[1][i - 1]);
if (c == s[i])
f[1][i] = max(f[1][i], f[0][i - 1] + 1);
}
int res = 0;
for (int i = 1; i <= m; i++)
res += (f[1][i] - f[1][i - 1]) << (i - 1);
return res;
}
int main()
{
cin >> n >> m;
scanf("%s", s + 1);
for (int mask = 0; mask < (1 << m); mask++)
for (int k = 0; k < 3; k++)
trans[mask][k] = DP(mask, cset[k]);
int cur = 0, lst = 1;
F[cur][0][0] = 1;
for (int i = 1; i <= n; i++)
{
swap(cur, lst);
memset(F[cur], 0, sizeof(F[cur]));
for (int k = 0; k < 3; k++)
{
int to2 = k + 1, fro = k, to = k == 0 ? 1 : 0; // fro ? ->to2 : ->to
for (int j = 0; j < 3; j++)
for (int mask = 0; mask < (1 << m); mask++)
add(F[cur][j == fro ? to2 : to][trans[mask][k]], F[lst][j][mask]);
}
}
for (int j = 0; j < 3; j++)
for (int mask = 0; mask < (1 << m); mask++)
add(ans[__builtin_popcount(mask)], F[cur][j][mask]);
for (int i = 0; i <= m; i++)
printf("%d\n", ans[i]);
return 0;
}
[ZJOI2019] 麻将
学长推荐的 dp of dp 好题,我比较菜还没写(你不就是懒得写呗),读者可以自己尝试一下。

浙公网安备 33010602011771号