前置知识:dp 是啥你总得知道吧()
dp 套 dp,又叫 dp of dp,此类题目一般以一个 dp 问题为方案,求方案数。比如求最长上升子序列长度为 \(x\) 的方案数。
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。一种很 naive 的想法是,记录 \(S\) 遍历到 \(i\),\(T\) 遍历到 \(j\),LCS 为 \(k\) 的方案数。这个显然不行啊,因为至少要选一个 \(i/j\) 重新遍历。
那既然每次都要重新更新一遍内层 dp 数组,我们把 dp 数组存一下不就好了,正好把 \(k\) 那维也省掉了。
思路
设 \(dp_{x,y}\) 表示 \(T\) 中到\(x\),\(S\) 中到 \(y\);\(f_{i,S}\) 表示,\(T\) 中遍历到 \(i\),\(dp_{i}\) 的 \(|S|\) 个值。
但是 \(dp_{i}\) 数组怎么状压?总不能按进制存吧。
观察一下,不难发现值单调不降,并且前后两项最多差 \(1\)。所以可以把它差分一下,变成一个二进制 \(mask\)!
这样一来,枚举 \(i\) 以及 \(T_i\),枚举 \(i-1\) 轮的 \(mask\),然后把 \(mask\) 前缀和一下,对它使用学过的 LCS 转移,最后差分回去为 \(mask'\)。则 \(f_{i-1,mask}\) 对 \(f_{i,mask'}\) 产生贡献。
最后统计答案部分我不说你也会。
问题就解决了。
但时间复杂度为 \(\mathcal{O}(m \times |字符集| \times 2^{|S|} \times |S|)\),有点悬啊!
优化
这时,再考虑进行一个很简单的优化:
注意到对于相同的参数 \(T_i\) 和 \(mask\),返回的 \(mask'\) 一样。所以把所有的情况先预处理一遍,求 \(f\) 的时候就不用每次算了!
时间复杂度 \(\mathcal{O}(|字符集|\times 2^{|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, dp[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(dp, 0, sizeof(dp));for (int i = 1; i <= m; i++)dp[0][i] = dp[0][i - 1] + ((mask >> (i - 1)) & 1);for (int i = 1; i <= m; i++){dp[1][i] = max(dp[0][i], dp[1][i - 1]);if (c == s[i])dp[1][i] = max(dp[1][i], dp[0][i - 1] + 1);}int res = 0;for (int i = 1; i <= m; i++)res += (dp[1][i] - dp[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, dp[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(dp, 0, sizeof(dp));for (int i = 1; i <= m; i++)dp[0][i] = dp[0][i - 1] + ((mask >> (i - 1)) & 1);for (int i = 1; i <= m; i++){dp[1][i] = max(dp[0][i], dp[1][i - 1]);if (c == s[i])dp[1][i] = max(dp[1][i], dp[0][i - 1] + 1);}int res = 0;for (int i = 1; i <= m; i++)res += (dp[1][i] - dp[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 : ->tofor (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 好题,我比较菜还没写(你不就是懒得写呗),读者可以自己尝试一下。