题解 P4590 【[TJOI2018]游园会】
shadowice1984 · · 题解
一个有趣的技术名字叫做dp套dp
基本是道板子题,只是注意直接开这么大的数组会炸请注意使用滚动数组进行优化
首先翻译一下并不好懂的题意
题目的意思是对于每一个i求满足如下条件的字符串的数目
1.长度为n且只出现过'N','O','I'三种字符
2.和一个长度为k的模式串的最长公共子序列长度恰好为i
3.不含"NOI"这个子串
然后这是一道对于字符串进行计数的题目那么我们考虑采取设一个
对于字符串题的话我们满足别的限制基本就是建一个自动机来搞定别的限制条件……像什么Ac自动机后缀自动机之类的都可以帮我们搞定一些但不是全部的限制条件
但是呢,没有"NOI”子串这个限制条件我们似乎可以轻松的记一维
但是……和一个模式串的最大子序列长度恰好为i这个限制条件就变得辣手了,因为我们并没有一个类似于“最大子序列自动机”的东西
但是仔细想想真的没有嘛?
除了自动机,还有什么东西是可以通过每次加一个字符实现转移的呢……?
当然是dp了~
所以让我们来考虑一下两个串求最长公共子序列的过程
设
那么我们可以得到一个转移方程是
接下来我们对这个转移方程以及dp数组做若干手脚,人工制造出一个“最长子序列自动机”来
首先我们认为
那么假设
现在的瓶颈明显出在我们需要将一个数组视为子序列自动机的一个节点这个问题上 我们需要考虑一下是不是所有的dp数组都是合法的
然后我们认真的考虑一下发现有很大一部分数组是不合法的
因为
然后还是老样子枚举点枚举出边字符使用dp的转移方程计算转移点就可以了
此时我们就可以设
然后无脑的跑一边dp就行了
当然,你可以不必把自动机建出来,而是每次转移的时候现场dp一次计算转移到的状态,这样也是可以通过本题的(6000ms时限随便过)这也是这种技术被称为dp套dp的原因
好了代码实际上是比你想象的好写的
上代码
// luogu-judger-enable-o2
#include<cstdio>
#include<algorithm>
using namespace std;const int N=1e3+10;typedef long long ll;const ll mod=1e9+7;
int n;int k;char mde[100];int dp[2][35000][3];int tr1[100];int tr2[100];int siz[35000];ll ans[100];
inline int hsh(int* a){int ret=0;for(int i=0;i<k;i++)ret|=(a[i+1]-a[i])<<i;return ret;}
inline void dhsh(int* a,int ret)//数组和数之间的转化
{
for(int i=0;i<k;i++)a[i+1]=(ret>>i)&1;
for(int i=1;i<=k;i++)a[i]+=a[i-1];
}
inline void dypr(int ti,int cf,int tj,char c,const ll& val)//转移函数
{
dhsh(tr1,cf);//现场解压数组之后压缩
for(int i=1;i<=k;i++)tr2[i]=max(max(tr2[i-1],tr1[i]),tr1[i-1]+1*(c==mde[i]));
int tcf=hsh(tr2);(dp[ti][tcf][tj]+=val)%=mod;
}
int main()
{
scanf("%d%d",&n,&k);scanf("%s",mde+1);//注意滚动数组
for(int i=1;i<=32767;i++)siz[i]+=siz[i>>1]+(i&1);dp[0][0][0]=1;
for(int i=0;i<n;i++)
{
int ti=(~i)&1;int ni=i&1;
for(int j=0;j<(1<<k);j++)
for(int p=0;p<3;p++)dp[ti][j][p]=0;
for(int j=0;j<(1<<k);j++)//暴力枚举下一个字符进行转移
{
if(dp[ni][j][0]!=0)
{
dypr(ti,j,1,'N',dp[ni][j][0]);
dypr(ti,j,0,'O',dp[ni][j][0]);
dypr(ti,j,0,'I',dp[ni][j][0]);
}
if(dp[ni][j][1]!=0)
{
dypr(ti,j,1,'N',dp[ni][j][1]);
dypr(ti,j,2,'O',dp[ni][j][1]);
dypr(ti,j,0,'I',dp[ni][j][1]);
}
if(dp[ni][j][2]!=0)
{
dypr(ti,j,1,'N',dp[ni][j][2]);
dypr(ti,j,0,'O',dp[ni][j][2]);
}
}
}
for(int i=0;i<(1<<k);i++)//最长子序列长度自然就是01串中1的个数,因为这是dp数组中的最大值
for(int p=0;p<3;p++)(ans[siz[i]]+=dp[n&1][i][p])%=mod;
for(int i=0;i<=k;i++)printf("%lld\n",ans[i]);return 0;//拜拜程序~
}