dp套dp学习笔记
简介
我都还不会怎么简介啊。
注,该篇是对着 dp套dp学习笔记 - dead_X - 博客园 (cnblogs.com) 抄的,如有雷同,并非巧合。
dp 和 dp套dp 的关系
- dp套dp 实际上就是将内层 dp 的结果作为外层 dp 的状态。
感觉根据这个定义根本搞不懂呢。
举个例子
题目大意
给定一个由 \(\text{AGCT}\) 组成的字符串 \(S\) ,考虑有多少长度为 \(n\) 的由 \(\text{AGCT}\) 组成的字符串满足他们的最长公共子序列长度等于 \(i\in[0,|S|]\) ,\(|S|\le 15,n\le 1000\) 。
分析
我们首先回忆一波 \(\text{LCS}\) 怎么求,对于一般的最长公共子序列,我们只有 \(O(n^2)\) 的解法,具体如下:
这很显然。我们考虑就利用这个 dp 过程来作为我们外层 dp 的状态。
我们不妨先尝试用暴力的做法来实现这个东西,就假设我们在做暴力,那么我们考虑的是暴力枚举当前的第二个串,然后用 dp 去 check 其的 \(\text{LCS}\) 。
我们就考虑设计方法二的 dp 状态,具体的用 \(f_{T,i}\) 表示第二个串是 \(T\) ,长度为 \(|T|\) ,第一个串匹配到 \(i\) 的匹配值最大是多少。转移的话每一次枚举添加的字符即可。
考虑上面这个做法如何优化?实际上我们发现 \(|T|\) 的状态是一定向 \(|T|+1\) 的位置转移的。换个想法,我们可以另设一个状态 \(g_T\) 表示第二个串为 \(T\) 的时候,\(i\in[0,|S|]\) 的取值,这个 \(g_T\) 也是可以直接 dp 转移的。
由于题目让我们求的是对应长度的方案数,我们不妨尝试将 \(\text{dp}\) 数组的状态与结果的位置交换(这个 \(\text{trick}\) 似乎以前见过),即 \(g_{\{a_i\}}\) 表示 \(i\in[0,|S|]\) 时值为 \(a_i\) 的情况下的方案数,我们考虑这个东西也是可以转移的,且状态数是 \(O(|S|^{|S|})\) 的。我们如果考虑暴力在这个 DAG 上跑长度为 \(n\) 的路径方案数,复杂度是 \(O(n|S|^{|S|})\) 的,暂且不能通过。
然后发现数组 \(a_i\) 中有很多状态是无用状态,具体观察性质可以发现,其相邻两位的差只能为 \(0,1\) ,所以我们可以通过差分 \(a_i\) 来进一步减少状态,最终复杂度应该就是 \(O(n2^{|S|})\) 的。
#include<bits/stdc++.h>
using namespace std;
const int N=17;
const int MOD=1e9+7;
int ADD(int x,int y){return x+y>=MOD?x+y-MOD:x+y;}
int TIME(int x,int y){return (int)(1ll*x*y%MOD);}
int n,m;
char s[N],t[4]={'A','G','C','T'};
int g[4][1<<N],f[2][1<<N],res[N];
int solve(){
scanf("%s%d",s,&m),n=strlen(s);
for(int i=0;i<(1<<n);++i){
int a[N],b[N];a[0]=(i&1);
for(int j=1;j<n;++j) a[j]=a[j-1]+((i>>j)&1);
for(int c=0;c<4;++c){
b[0]=max(a[0],(int)(s[0]==t[c]));
for(int j=1;j<n;++j)
b[j]=max(max(b[j-1],a[j]),a[j-1]+(s[j]==t[c]));
int I=b[0];
for(int j=1;j<n;++j) I|=((b[j]-b[j-1])<<j);
g[c][i]=I;
}
}
f[0][0]=1;
for(int j=1;j<(1<<n);++j) f[0][j]=0;
for(int i=1;i<=m;++i){
for(int j=0;j<(1<<n);++j) f[i&1][j]=0;
for(int j=0;j<(1<<n);++j){
for(int c=0;c<4;++c)
f[i&1][g[c][j]]=ADD(f[i&1][g[c][j]],f[(i-1)&1][j]);
}
}
for(int i=0;i<=n;++i) res[i]=0;
for(int i=0;i<(1<<n);++i){
int cnt=0,I=i;while(I) cnt+=(I&1),I>>=1;
res[cnt]=ADD(res[cnt],f[m&1][i]);
}
for(int i=0;i<=n;++i) printf("%d\n",res[i]);
return 0;
}
int main(){
int T;cin>>T;while(T--) solve();
return 0;
}
总结
我们再总体回顾一下上面的过程,除去最后的状压优化,前面的 dp 推导过程实际上是和很能体现 dp套dp 的性质的,具体的我们发现实际上我们就是通过先一步的 dp ,算出我们再添加一个字符的情况下,能够转移到的 dp 位置,这为我们后面的交换状态与结果的 dp 提供了转移的便捷性,然后我们再进行后者的 dp 。
求一个 dp 结果的方案数,我们可以利用 dp套dp 。
例题2
题目大意
给定一个由 \(\text{NOI}\) 组成的字符串 \(S\) ,考虑有多少长度为 \(n\) 的由 \(\text{NOI}\) 组成的字符串满足他们的最长公共子序列长度等于 \(i\in[0,|S|]\) ,且不存在子串 \(\text{NOI}\) ,\(|S|\le 15,n\le 1000\) 。
分析
基本做法可以确定和上面是类似的,只不过需要多加两维记录一下不要连续以 \(\text{NOI}\) 的形式走即可。
代码略。
例题3
咕咕咕。