dp套dp学习笔记

简介

我都还不会怎么简介啊。

注,该篇是对着 dp套dp学习笔记 - dead_X - 博客园 (cnblogs.com) 的,如有雷同,并非巧合。

dp 和 dp套dp 的关系

  • dp套dp 实际上就是将内层 dp 的结果作为外层 dp 的状态。

感觉根据这个定义根本搞不懂呢。

举个例子

BZOJ3864 Hero meet devil

题目大意

给定一个由 \(\text{AGCT}\) 组成的字符串 \(S\) ,考虑有多少长度为 \(n\) 的由 \(\text{AGCT}\) 组成的字符串满足他们的最长公共子序列长度等于 \(i\in[0,|S|]\)\(|S|\le 15,n\le 1000\)

分析

我们首先回忆一波 \(\text{LCS}\) 怎么求,对于一般的最长公共子序列,我们只有 \(O(n^2)\) 的解法,具体如下:

\[f_{i,j}=\left\{\begin{matrix} f_{i-1,j-1}+1 & A_i=B_j \\ \max(f_{i-1,j},f_{i,j-1}) & A_i\ne B_j \end{matrix}\right. \]

这很显然。我们考虑就利用这个 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

P4590 [TJOI2018]游园会

题目大意

给定一个由 \(\text{NOI}\) 组成的字符串 \(S\) ,考虑有多少长度为 \(n\) 的由 \(\text{NOI}\) 组成的字符串满足他们的最长公共子序列长度等于 \(i\in[0,|S|]\) ,且不存在子串 \(\text{NOI}\)\(|S|\le 15,n\le 1000\)

分析

基本做法可以确定和上面是类似的,只不过需要多加两维记录一下不要连续以 \(\text{NOI}\) 的形式走即可。

代码略。

例题3

P5279 [ZJOI2019]麻将

咕咕咕。

posted @ 2021-12-23 14:12  Point_King  阅读(438)  评论(0编辑  收藏  举报