Luogu4590 [TJOI2018]游园会
Luogu4590 [TJOI2018]游园会
\(DP\)套\(DP\)
设\(dp_{i,j,k}\)表示选到第\(i\)个字符,匹配状态为\(j\),\(NOI\)子串已经匹配了\(k\)个的方案数。
问题在于\(j\)无法用字符串知识解决。
由于\(k\)很小,我们可以考虑\(dp\)来求状态。
最长公共子序列的\(dp\)方程为:
\[
dp_{i,j}=\max \{ dp_{i-1,j} , dp_{i,j-1} , dp_{i-1,j-1}+1\}
\]
我们已经知道了模式串,我们先用一个数组来表示状态。
\(a_i\)表示当前状态下,在模式串前\(i\)个位置匹配的最长长度。
现在我们加入一个字符\(c\),我们需要把\(a\)更新为\(b\)。
\[b_i=\max \{ a_i , b_{i-1} , a_{i-1} + [c=s_i]\}
\]
考虑\(dp\)方程的含义,\(a_i\)表示不考虑加入新字符后原来的值,\(b_{i-1}\)表示不用当前字符在第\(i\)位匹配,利用前\(i-1\)位得到的新\(dp\)值,\(a_{i-1} + [c=s_i]\)即在当前位匹配。
现在我们就需要把\(b\)数组压缩成一个状态,那么\(15\)个数,每个位置取值\([0,15]\),总状态数为……
\[16^{15}=2^{60}
\]
出大问题!
显然有许多多余状态,利用最长公共子序列的性质\(b_i-b_{i-1} \in \{ 0,1\}\),即最多可能多匹配一位,差分以后压缩成二进制数,状态数降为\(2^k\)。
每次\(dp\)时解压状态,然后压缩回去即可。
一开始模数写成\(998244353\),直接挂成\(10pts\)。
\(Code:\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int p=1000000007;
int n,k,cur,pre;
int dp[2][1 << 15][3];
int a[25],b[25],ans[25],ct[1 << 15];
char s[25];
void add(int &x,int y)
{
x=(x+y)%p;
}
void hash_back(int x)
{
a[0]=0;
for (int i=1;i<=k;++i)
a[i]=a[i-1]+((x >> i-1) & 1);
}
int get_hash()
{
int s=0;
for (int i=1;i<=k;++i)
s+=(1 << i-1)*(b[i]-b[i-1]);
return s;
}
void Dp(int val,char c,int fr,int opt)
{
hash_back(fr);
b[0]=0;
for (int i=1;i<=k;++i)
b[i]=max(max(a[i],b[i-1]),a[i-1]+(c==s[i]));
int t=get_hash();
add(dp[cur][t][opt],val);
}
int main()
{
scanf("%d%d",&n,&k);
scanf("%s",s+1);
cur=0,pre;
dp[cur][0][0]=1;
for (int i=1;i<=n;++i)
{
pre=cur,cur^=1;
memset(dp[cur],0,sizeof(dp[cur]));
for (int j=0;j<(1 << k);++j)
{
if (dp[pre][j][0])
Dp(dp[pre][j][0],'N',j,1),Dp(dp[pre][j][0],'O',j,0),Dp(dp[pre][j][0],'I',j,0);
if (dp[pre][j][1])
Dp(dp[pre][j][1],'N',j,1),Dp(dp[pre][j][1],'O',j,2),Dp(dp[pre][j][1],'I',j,0);
if (dp[pre][j][2])
Dp(dp[pre][j][2],'N',j,1),Dp(dp[pre][j][2],'O',j,0);
}
}
for (int i=1;i<(1 << k);++i)
ct[i]=ct[i >> 1]+(i & 1);
for (int i=0;i<(1 << k);++i)
for (int j=0;j<3;++j)
add(ans[ct[i]],dp[cur][i][j]);
for (int i=0;i<=k;++i)
printf("%d\n",ans[i]);
return 0;
}