DP 套 DP 与 游园会
DP 套 DP
听名字猜不到它是个什么东西。
接下来用一道例题 P459 TJOI2018 游园会 来解释 DP 套 DP。
游园会
参考资料。
题目描述
小豆参加了 NOI 的游园会,会场上每完成一个项目就会获得一个奖章,奖章只会是 \(\texttt{N}\)、\(\texttt{O}\)、\(\texttt{I}\) 的字样。在会场上他收集到了 \(K\) 个奖章组成的串。兑奖规则是奖章串和兑奖串的最长公共子序列长度为小豆最后奖励的等级。现在已知兑奖串长度为 \(N\),并且在兑奖串上不会出现连续三个奖章为 \(\texttt{NOI}\),即奖章中不会出现子串 \(\texttt{NOI}\)。现在小豆想知道各个奖励等级会对应多少个不同的合法兑奖串。
输入格式
第一行两个数,\(N, K\) 分别代表兑奖串的长度,小豆收集的奖章串的长度(\(N\leq1000,K\leq15\))。
第二行一共 \(K\) 个字符,表示小豆得到的奖章串。
输出格式
一共 \(K+1\) 行,第行表示小豆最后奖励等级为 \(i-1\) 的不同的合法兑奖串的个数,可能这个数会很大,结果对 \(10^9+7\) 取模。
数据范围
- \(N\leq1000,K\leq15\)。
题目大意
对于每个 \(i\) 求满足以下条件的字符串数量:
- 长度为 \(n\),字符集为 \(N,O,I\)。
- 不包含子串
NOI
。 - 与给定的长为 \(k\) 的字符串的最长公共子序列的长度为 \(i\)。
分析 & 解法
每个位置都可以选其中一种字符,求最后的方案数。
这样的问题考虑 DP 求解,好处是我们不用考虑可能的重复计数的情况,因为 DP 的转移保证了包含所有情况。
考虑设 \(dp_{i,...}\),表示长度为 \(i\) 的字符串,后面有一些奇奇怪怪的状态限制。那么接下来就枚举下一个字符是 \(N,O,I\) 中的哪一个。
我们看不包含子串 NOI
这个限制,我们可以记录 \(0/1/2\) 表示当前字符串的后缀匹配 NOI
的长度。
那么问题就是 与给定的长为 k 的字符串的最长公共子序列的长度为 i
,看起来很棘手。
如果是一般的字符串限制,我们可以考虑记录当前字符串匹配到对应的自动机上的节点编号,将编号作为 DP 状态。这样我们在转移 DP时,选取一个新字符,就可以知道在自动机上转移后新的节点编号。
或者反过来在自动机上做 DP。
然而我们并没有 最长公共子序列自动机
。
除了自动机,那有什么东西加一个字符可以转移呢?就是 DP。
我们考虑大家是怎么做最长公共子序列的长度的。
设 \(f_{i,j}\) 表示 \(A\) 的前 \(i\) 个字符和 \(B\) 的前 \(j\) 个字符的答案,则显然有:
那我们就类似于记录自动机上的节点编号。
当字符串长度为 \(i\) 时,我们考虑记录 \(f_i\) 这个一维数组长什么样,作为状态。
首先在这里,我们的 \(B\) 串是固定已知的,我们每次转移时就要知道:\(f_{i-1}\) 这个一维数组长什么样,\(A_i\) 是什么。
\(f_{i-1}\) 已经设在状态里了,\(A_i\) 正是我们枚举的当前这一位置选什么。
至此 DP 套 DP 的雏形就出来了。
我们设 DP 状态 \(dp_{i,f_i,0/1/2}\),这里 \(f_i\) 实际是一个编号,它代表一个 DP 数组。
也就是说,我们把一个 DP 设为另一个 DP 的状态,这就是 DP 套 DP 名字的由来。
然而,到这里远没结束,我们发现我们这个 \(f_i\) 的种类实在太多了,考虑去掉无用状态,这就是 DP 套 DP 都要做的。
很多 \(f_i\) 不合法。
-
根据 \(f\) 的定义,对于任意 \(i\),\(f_i\) 这个数组是单调的。
-
进一步地,\(f_i\) 相邻两项最多相差 \(1\)。
于是我们把原来的数组的差分表达为一个 \(01\) 串。
然后将 \(01\) 串视为一个数的二进制表示,于是我们的节点数最多为 \(O(2^k)\)。
用上面所说的状态跑一遍 DP 即可。
状态数为 \(O(3\times n\times2^k)\),需要滚动数组。
另外,我们可以像自动机一样,对 \(f_i\) 预处理每个节点选一个新的节点后的后继节点。
当然,你也可以选择每次当场转移后继节点。
我们的时间还要乘上枚举的新字符数,即 \(3\)。
总时间复杂度 \(O(3\times 3\times n\times 2^k)\),空间复杂度 \(O(3\times 2^k)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,K;
char B[20];
int a[20];
void add(int &x,int y){
x=(x+y)%mod;
}
int nx[1<<16][3];
int f[2][1<<16][3];
int ans[20];
#define popcnt(x) __builtin_popcount(x)
int main(){
cin>>n>>K;
scanf("%s",B+1);
for(int i=1;i<=K;i++){
if(B[i]=='N')a[i]=0;
else if(B[i]=='O')a[i]=1;
else a[i]=2;
}
for(int i=0;i<1<<K;i++){
for(int k=0;k<3;k++){
int tmp[20];
tmp[0]=0;
for(int j=1,x=i;j<=K;j++,x>>=1){
tmp[j]=x&1;
tmp[j]+=tmp[j-1];
}
int nw[20];
nw[0]=0;
for(int j=1;j<=K;j++){
nw[j]=max(max(nw[j-1],tmp[j]),tmp[j-1]+(a[j]==k));
int x=nw[j]-nw[j-1];
if(x)nx[i][k]+=1<<j-1;
}
}
}
f[0][0][0]=1;
for(int i=1;i<=n;i++){
memset(f[i&1],0,sizeof f[i&1]);
for(int k=0;k<3;k++){
for(int l=0;l<3;l++){
int nk;
if(k==l)nk=k+1;
else{
nk=0;
if(l==0)nk=1;
}
if(nk<=2){
for(int j=0;j<1<<K;j++){
add(f[i&1][nx[j][l]][nk],f[(i&1)^1][j][k]);
}
}
}
}
}
for(int i=0;i<1<<K;i++){
add(ans[popcnt(i)],f[n&1][i][0]);
add(ans[popcnt(i)],f[n&1][i][1]);
add(ans[popcnt(i)],f[n&1][i][2]);
}
for(int i=0;i<=K;i++)cout<<ans[i]<<"\n";
}
大功告成!
于是我们就可以去刷题了!