dp 套 dp 学习笔记

dp 的本质:通过不同的转移更新状态的答案,就像 DAG 上的拓扑一样。

dp 套 dp 的本质:将内层 dp 的答案作为外层 dp 的状态进行转移。

比如某个 dp 的状态为 \(f_{i,j}\),第二维不再仅仅是某个状态,而是代表内层 dp 的答案,如 \(f_{i.\text{vector<int>}j}\)

当外层 dp 进行转移时,内层 dp 同时进行转移。

通常内层 dp 的答案会通过状压来在外层 dp 上表示。

[TJOI2018]游园会

题意:有一个长度为 \(n\) 的字符串 \(S\),其中不包含子串 NOI,和一个字符串 \(T\)。对于每一个 \(i\),求出 \(\text{LCS}(S,T)=i\)\(S\) 有多少种。

先不考虑子串限制的条件。

如果设 \(f_{i,j}\) 表示 \(S\) 的前 \(i\) 个字符和 \(T\) 匹配的 \(\text{LCS}\) 长度为 \(j\) 时的字符串数量,会发现无法转移,因为不知道当前状态匹配的 \(\text{LCS}\) 到了哪里,也就是说,我们需要把 \(\text{LCS}\) 的匹配结果作为状态表示出来。这时候就需要 dp 套 dp 了。

我们先看内层 dp,求 \(\text{LCS}\)

回想起 \(\text{LCS}\)\(O(n^2)\) 做法,我们设 \(f_{i,j}\) 表示前 \(S\) 的前 \(i\) 个字符和 \(T\) 的前 \(j\) 个字符匹配的 \(\text{LCS}\)

\[f_{i,j}= \left\{ \begin{aligned} f_{i-1,j-1}+1&&S_i=T_j\\ \max\{f_{i-1,j},f_{i,j-1}\}&&S_i\not= T_j\\ \end{aligned} \right. \]

发现每添加一个字符,相当于 \(f_i\) 转移到 \(f_{i+1}\),也就是说外层 dp 的第二维可以记为 \(f_i\) 这一层,每次转移时计算出 \(f_{i+1}\) 这一层就行。但是,这样时间和空间都爆炸了。

考虑优化,我们研究内层 dp,发现在 \(f_i\) 这一层中,\(f_{i,j+1}-f_{i,j}\in[0,1]\) ,差分出来就是一个 01 串,那么就可以对它进行状压了。

现在回到外层 dp 上,我们可以设 \(f_{i,j}\) 表示现在在 \(S\) 的第 \(i\) 位,匹配的 \(\text{LCS}\) 状压后成了 \(j\)。发现可以用滚动数组优化掉第一维。

现在重新来考虑子串限制的条件,我们只需要再开一维来记录匹配到了 NOI 的第 \(0/1/2\) 位就行。

时间复杂度为 \(O(k\times 2^k + n\times 2^k)\),空间复杂度为 \(O(n+2^k)\),常数都不小。

代码:

#include<bits/stdc++.h>
#define pc(x) putchar(x)
using namespace std;
inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){f=ch=='-'?-1:f;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    return x*f;
}
void write(int x)
{
    if(x<0){x=-x;putchar('-');}
    if(x>9)write(x/10);
    putchar(x%10+48);
}
const int mod=1e9+7;
char s[20];int mp[256];
int n,m,S,a[20],f[2][32770][3],ans[1005];
int g[2][1005],sta[32770][3],nxt[3][3]={{1,0,0},{1,2,0},{1,0,3}};
//f[i][j][k]表示前i个字符匹配的LCS的差分状压成j,NOI出现到了第0\1\2位
void init()//预处理出内层DP的转移
{
    for(int i=0;i<S;++i)
    {
        for(int j=1;j<=m;++j)g[0][j]=g[0][j-1]+((i>>(j-1))&1);
        for(int j=0;j<=2;++j)
        {
            for(int k=1;k<=m;++k)
            {
                g[1][k]=max(g[1][k-1],g[0][k]);
                if(a[k]==j)g[1][k]=max(g[1][k],g[0][k-1]+1);
            }int now=0;
            for(int k=1;k<=m;++k)now|=(1<<(k-1))*(g[1][k]>g[1][k-1]);
            sta[i][j]=now;
        }
    }
}
int main()
{
    n=read(),m=read();scanf("%s",s+1);S=(1<<m);
    mp['N']=0;mp['O']=1;mp['I']=2;
    for(int i=1;i<=m;++i)a[i]=mp[s[i]];//转序列
    init();f[0][0][0]=1;
    for(int i=1;i<=n;++i,memset(f[i&1],0,sizeof f[i&1]))
        for(int now=0;now<S;++now)
            for(int j=0;j<=2;++j)
                for(int k=0;k<=2;++k)
                    if(j!=2||k!=2)(f[i&1][sta[now][k]][nxt[j][k]]+=f[i&1^1][now][j])%=mod;
    for(int now=0;now<S;++now)
    {
        int tmp=now,cnt=0;while(tmp)cnt+=tmp&1,tmp>>=1;
        for(int j=0;j<=2;++j)
            (ans[cnt]+=f[n&1][now][j])%=mod;
    }
    for(int i=0;i<=m;++i)write(ans[i]),pc('\n');
    return 0;
}

[BJWC2018]最长上升子序列

题意:现在有一个长度为 \(n\) 的随机排列,求它的最长上升子序列长度的期望。

正解应该是杨表,但是 dp 套 dp 可以打表 AC。

从前往后 dp,发现不能维护状态进行转移,所以从小往大来 dp。

\(f_i\) 表示排列中第 \(i\) 个数结尾的 \(\text{LIS}\) 长度,\(mx_i\)\(f_i\) 的前缀最大值,可以发现 \(mx_i\le mx_{i+1}\le mx_i+1\)

所以我们可以差分加状压来维护这个 \(mx\) 数组。

考虑现在在 \(i\)\(i+1\) 间添加新数,那么这个数结尾的 \(\text{LIS}\) 长度一定为 \(mx_i+1\),由于上面的性质,所以差分数组中相当于在 \(i\) 位后面添加一个 \(1\),然后后面的遇到的第一个 \(1\) 变成 \(0\)(没有就不变)。

现在设计外层 dp,设 \(f_{i,j}\) 代表添加到 \(i\) 时,\(mx\) 的差分数组状压为 \(j\) 的排列数。

那么答案就是:

\[\sum_{i=0}^{2^n-1}f_{n,i}\times \text{__builtin_popcount}(i) \]

时间复杂度为 \(O(n^2\times 2^n)\),空间复杂度为 \(O(2^n)\)。显然都爆炸。

怎么办呢?本地打表,反正只有 \(28\) 个答案。

代码不贴了。

[NOI2022] 移除石子

补了,但不是很容易讲出来内层 dp 的设计。懒得写了(

posted @ 2022-09-07 14:16  violetctl39  阅读(144)  评论(0编辑  收藏  举报