[整理]一类有趣的问题——dp 套 dp
dp 套 dp 是怎么回事呢?dp 相信大家都很熟悉,但是 dp 套 dp 是怎么回事呢,下面就让小编带大家一起了解吧。dp 套 dp,其实就是把内层 dp 的结果作为外层 dp 的状态,大家可能会很惊讶 dp 怎么会套 dp 呢?但事实就是这样,小编也感到非常惊讶。这就是关于 dp 套 dp 的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!
虽然但是,上面这段话还是说出了 dp 套 dp 的精髓:把内层 dp 的结果作为外层 dp 的状态。
什么意思呢?有一类问题需要求满足某个条件的方案数,而满足条件的判定本身就是一个 dp,这时我们就可以尝试把这个 dp 的结果作为状态进行新的 dp。
其实这里内层 dp 本质上是一个自动机,状态是自动机的节点,转移是自动机的边。
例题一:BZOJ 3864。
这道题要求计算与给定串的 LCS 长度为 \(i\) 的个数。我们知道 LCS 是可以 dp 出来的(设 \(f_{i,j}\) 为第一个串的前 \(i\) 个和第二个串的前 \(j\) 个的 LCS),所以我们把这个 dp 的结果作为外层 dp 的状态,设 \(g_{i,S}\) 为前 \(i\) 个字符算出的 \(f\) 为 \(S\) 的方案数。
根据 \(f\) 的性质可以知道我们只需要保存最后一行的状态即可完成添加一个字符的转移,而这个状态可以差分后压起来。
最终我们得到了一个优美但是在最优解最后一页的 \(O(2^nm|\Sigma|)\) 代码:
const int N=1010,M=33000,p=1e9+7;
const char C[]={'A','G','C','T'};
int T,n,m;char s[20];
int tr[M][4],f[2][M],g[2][20];
il int Encode(){
int res=0;
for(int i=1;i<=n;i++){
if(g[1][i]>g[1][i-1])res|=(1<<i-1);
}
return res;
}
il void Decode(int S){
memset(g[0],0,sizeof g[0]);
for(int i=1;i<=n;i++)g[0][i]=g[0][i-1]+((S>>i-1)&1);
}
il void Trans(int S){
Decode(S),memset(g[1],0,sizeof g[1]);
for(int j=0;j<4;j++){
for(int i=1;i<=n;i++){
g[1][i]=max(g[1][i-1],max(g[0][i],\
(C[j]==s[i])*(g[0][i-1]+1)));
}
tr[S][j]=Encode();
}
}
int ans[N];
int main(){
Read(T);
while(T--){
scanf("%s",s+1),Read(m),n=strlen(s+1);
for(int S=0;S<(1<<n);S++)Trans(S);
memset(f,0,sizeof f),f[0][0]=1;
for(int i=1;i<=m;i++){
int nw=i&1,lt=nw^1;memset(f[nw],0,sizeof f[nw]);
for(int S=0;S<(1<<n);S++){
for(int j=0;j<4;j++){
(f[nw][tr[S][j]]+=f[lt][S])%=p;
}
}
}
memset(ans,0,sizeof ans);
for(int S=0;S<(1<<n);S++){
(ans[__builtin_popcount(S)]+=f[m&1][S])%=p;
}
for(int i=0;i<=n;i++)printf("%d\n",ans[i]);
}
KafuuChino HotoKokoa
}
可以看出,代码中的 Trans
函数完成了构建内层 dp 自动机的工作。
有一道类似的题目是[TJOI2018]游园会,在此题的基础上禁止了 \(\texttt{NOI}\) 出现,我们只需要加一维记录当前匹配到 \(\texttt{NOI}\) 的第几位即可。这下好了,直接掉到最劣解了
现在你已经学会了基本的 dp 套 dp,尝试一下[ZJOI2019]麻将吧