【洛谷4590】[TJOI2018] 游园会(DP套DP重入门)

点此看题面

大致题意: 给定一个字符串,对于每一个\(i\),问有多少由"N","O","I"组成的长度为\(m\)的字符串,满足其中不存在连续的"NOI",且和给定串的最长公共子序列长度为\(i\)

前言

今天看到机房里有人在做\(DP\)\(DP\)题,先是一脸懵逼觉得这是什么鬼东西,似乎从没听说过,结果回头转念一想发现去年\(ZJOI\)\(T1\)好像就是这个东西。。。

然后找到一道去年看到没写的\(DP\)\(DP\)题,重新学学这玩意。

\(DP\)\(DP\)

考虑我们在什么情况下会用到\(DP\)\(DP\)

一般就是为满足某种状态的东西\(DP\)计数,而这种状态不太好维护,又需要\(DP\)转移。

上面可能说得有点抽象,那么就以这题为例,题目中要求\(LCS\),可如果单纯地\(DP\)很难直接维护出\(LCS\)的信息。而众所周知,\(LCS\)这东西本身是可以\(DP\)求的,因此就需要\(DP\)\(DP\)

实际上,针对这种问题,有一个比较容易理解的做法,就是对于一层\(DP\)改为建立一个自动机,而另一层\(DP\)改为在自动机上\(DP\)(相信大家都会\(AC\)自动机上\(DP\)吧),例如这题我们就可以建立一个\(LCS\)自动机。

\(LCS\)自动机

\(f_{now,i}\)表示当前串与给定串前\(i\)位的\(LCS\),则\(f_{lst,i}\)就表示前一个串与给定串前\(i\)位的\(LCS\),并令\(x\)为当前加入的字符。

考虑\(LCS\)的基本转移方程:(其中\(s\)为给定串)

\[f_{now,i}=max\{f_{lst,i},f_{now,i-1},f_{lst,i-1}+[x=s_i]\} \]

不难发现,每一种\(f\)数组就对应着\(LCS\)自动机上的一个状态。

你可以像麻将那题一样直接用\(map\)来维护,不过实际上这题有一种复杂度更优秀的做法,就是状压。

考虑相邻的\(f_{i-1}\)\(f_i\)之间最多相差\(1\),于是我们只要状压差值,就可以高效地压缩一个状态了。

建立这个自动机时,我们可以令\(S_{x,w}\)表示状态\(x\)在加入字符\(w\)之后会转移到哪一状态。

自动机上\(DP\)

仿照一般自动机上\(DP\)的套路,设\(f_{i,j,0/1/2}\)表示当前为第\(i\)个字符、在自动机上的\(j\)号点、连续出现了"NOI"中\(0/1/2\)个字符的方案数。

转移只要枚举当前加入的字符\(w\),就可以转移到\(f_{i+1,S_{j,w},0/1/2}\)(其中第三维只要分保持原先的连续和重新开始两种情况讨论即可)。

这一过程实际上还是比较简单的,具体实现详见代码。

代码

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define M 15
#define X 1000000007
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,l,s[M+5];char st[M+5];
class LCS_Automation
{
	private:
		int lim,a[M+5],f[M+5],S[1<<M][3],cnt[1<<M],dp[2][1<<M][3];
		I void Extend(CI x)//求出x的后继状态
		{
			RI i;for(i=0;i^l;++i) a[i+1]=a[i]+((x>>i)&1);//解码
			RI j,t;for(i=0;i^3;++i)//枚举加入字符
			{
				for(j=1;j<=l;++j) f[j]=max(max(a[j],f[j-1]),a[j-1]+(s[j]==i));//DP
				for(t=j=0;j^l;++j) t|=(f[j+1]-f[j])<<j;S[x][i]=t;//状压差值
			}
		}
	public:
		I void Build() {lim=1<<l;for(RI i=0;i^lim;++i) Extend(i);}//建立LCS自动机
		int ans[M+5];I void DP()//LCS自动机上DP
		{
			#define Cls(t) memset(dp[t],0,sizeof(dp[t]))
			RI i,j,k,w,t;for(Cls(0),dp[0][0][0]=1,t=i=0;i^n;++i,t^=1) for(Cls(t^1),j=0;j^lim;++j)//注意滚存
				for(k=0;k^3;++k) for(w=0;w^3;++w) (k^2||w^2)&&Inc(dp[t^1][S[j][w]][k^w?!w:k+1],dp[t][j][k]);//枚举加入字符转移
			for(i=0;i^lim;++i) cnt[i]=cnt[i>>1]+(i&1),//显然cnt[i]就是状态i的LCS
				Inc(ans[cnt[i]],dp[t][i][0]),Inc(ans[cnt[i]],dp[t][i][1]),Inc(ans[cnt[i]],dp[t][i][2]);//统计答案
		}
}A;
int main()
{
	RI i;for(scanf("%d%d%s",&n,&l,st+1),i=1;i<=l;++i) s[i]=st[i]^'N'?(st[i]^'O'?2:1):0;//把字符改为数码
	for(A.Build(),A.DP(),i=0;i<=l;++i) printf("%d\n",A.ans[i]);return 0;//建自动机,然后自动机上DP
}
posted @ 2020-06-15 12:34  TheLostWeak  阅读(20)  评论(0编辑  收藏