【学习笔记】DP 套 DP
内外层 DP
大致一个经典的形式是“求……值为……的方案数”,特点是对于一个给定的情况,我们需要 DP 求出其对应的值,而题目要求对于所有情况求出对应方案数。
换言之内层是求出值是多少或者判断是否合法的判定 DP,而外层是求方案数的计数 DP。
外层 DP 是在根据内层 DP 值建出的 DFA 上进行的。
一道题
求与给定字符串的 \(\mathrm{LCS}\) 长为 \(0\sim|S|\) 的方案数。
求 \(\mathrm{LCS}\) 是一个经典 DP,设 \(f_{i,j}\) 为前 \(i\) 位与给定字符串 \(S\) 前 \(j\) 位的 \(\mathrm{LCS}\),转移方程:
发现关于 \(f_i\) 的一行只需要已知 \(T_i\) 以及关于 \(f_{i-1}\) 的一行即可,且 \(f_i\) 一行相邻两位只增加 \(0\) 或 \(1\),可以把这个差分压成一个状态,还原成原数组后结合当前字符就可以得到下一行。
于是可以建出这个 DFA。
外层 DP 就是在 DFA 上进行的,设 \(DP_{i,s}\) 表示长度为 \(i\) 的字符串匹配到状态 \(s\) 的方案数。
剩下一个限制增加一维特判即可。
点击查看代码
int n,m;
char S[20];
int mp[128];
int popcount[maxn];
int trans[maxn][3];
int pre[20],now[20];
int dp[2][maxn][3];
inline void build(){
for(int s=0;s<(1<<m);++s){
pre[0]=0;
for(int i=1;i<=m;++i) pre[i]=pre[i-1]+((s>>i-1)&1);
for(int c=0;c<3;++c){
now[0]=0;
for(int i=1;i<=m;++i){
now[i]=max(pre[i],now[i-1]);
if(mp[S[i]]==c) now[i]=max(now[i],pre[i-1]+1);
}
for(int i=1;i<=m;++i) trans[s][c]|=((now[i]-now[i-1])<<i-1);
}
}
}
int ans[20];
int main(){
mp['N']=0,mp['O']=1,mp['I']=2;
for(int i=1;i<(1<<15);++i) popcount[i]=popcount[i>>1]+(i&1);
n=read(),m=read();
scanf("%s",S+1);
build();
dp[0][0][0]=1;
for(int i=0;i<n;++i){
for(int s=0;s<(1<<m);++s){
for(int c=0;c<3;++c){
if(!c){
dp[i&1^1][trans[s][c]][1]=(dp[i&1^1][trans[s][c]][1]+dp[i&1][s][0])%mod;
dp[i&1^1][trans[s][c]][1]=(dp[i&1^1][trans[s][c]][1]+dp[i&1][s][1])%mod;
dp[i&1^1][trans[s][c]][1]=(dp[i&1^1][trans[s][c]][1]+dp[i&1][s][2])%mod;
}
else if(c==1){
dp[i&1^1][trans[s][c]][0]=(dp[i&1^1][trans[s][c]][0]+dp[i&1][s][0])%mod;
dp[i&1^1][trans[s][c]][2]=(dp[i&1^1][trans[s][c]][2]+dp[i&1][s][1])%mod;
dp[i&1^1][trans[s][c]][0]=(dp[i&1^1][trans[s][c]][0]+dp[i&1][s][2])%mod;
}
else{
dp[i&1^1][trans[s][c]][0]=(dp[i&1^1][trans[s][c]][0]+dp[i&1][s][0])%mod;
dp[i&1^1][trans[s][c]][0]=(dp[i&1^1][trans[s][c]][0]+dp[i&1][s][1])%mod;
}
}
}
for(int s=0;s<(1<<m);++s) dp[i&1][s][0]=dp[i&1][s][1]=dp[i&1][s][2]=0;
}
for(int s=0;s<(1<<m);++s){
for(int k=0;k<3;++k){
ans[popcount[s]]=(ans[popcount[s]]+dp[n&1][s][k])%mod;
}
}
for(int i=0;i<=m;++i) printf("%d\n",ans[i]);
return 0;
}
另一道题
CodeForces-979E Kuro and Topological Parity *2400
内层 DP 写出来是:
取模之后 \(f\) 数组状态也有 \(2^{50}\),很难像上一题操作,然而判断结果是否合法时是对 \(f\) 求一个前缀和,且连边并没有特殊要求,所以并不关心 \(f\) 的具体样子。
好路径为偶数的点连出的边是任意连的,好路径为奇数的点连出的边有奇偶性的限制,除此之外还有一个颜色的限制(同色不计入结果,因此同色任意连),于是只需要维护前 \(i\) 个点中有 \(j\) 个好路径为奇数的白点,\(k\) 个好路径为奇数的黑点,\(l\) 个好路径为偶数的白点,剩余的 \(i-j-k-l\) 个点是好路径为偶数的黑点,这样复杂度是 \(O(n^4)\)。
好路径为偶数的点对任意的颜色都是没有影响的,所以最后一维也可以略去,复杂度 \(O(n^3)\)。
所以 DP 套 DP 并不需要建出 DFA,只是需要在外层 DP 状态设计中表示出内层的当前状态。