【学习笔记】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\),那刚刚的过程就可以形象地表示如下:

\[T_i, \{f_{i-1,j}\} \rightarrow trans() \rightarrow \{f_{i,j}\} \]

这就是外 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\),转移是

\[F_{i-1,mask} \xrightarrow{+} F_{i,trans(T_i,mask)} \]

内部枚举 \(j\),转移是

\[\begin{cases} f_{i-1,j-1} +1\xrightarrow{\max} f_{i,j} &T_i = S_j\\ \max(f_{i-1,j},f_{i,j-1})\xrightarrow{\max} f_{i,j} &else \end{cases} \]

最后统计答案就看 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 好题,我比较菜还没写(你不就是懒得写呗),读者可以自己尝试一下。

posted @ 2025-02-02 18:30  Aquizahv  阅读(32)  评论(0)    收藏  举报