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\) 转移到 \(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\) 的排列数。
那么答案就是:
时间复杂度为 \(O(n^2\times 2^n)\),空间复杂度为 \(O(2^n)\)。显然都爆炸。
怎么办呢?本地打表,反正只有 \(28\) 个答案。
代码不贴了。
[NOI2022] 移除石子
补了,但不是很容易讲出来内层 dp 的设计。懒得写了(