Loading

【学习笔记】DP 套 DP

Page Views Count

内外层 DP

大致一个经典的形式是“求……值为……的方案数”,特点是对于一个给定的情况,我们需要 DP 求出其对应的值,而题目要求对于所有情况求出对应方案数。

换言之内层是求出值是多少或者判断是否合法的判定 DP,而外层是求方案数的计数 DP。

外层 DP 是在根据内层 DP 值建出的 DFA 上进行的。

一道题

Luogu-P4590 TJOI 2018 游园会

求与给定字符串的 \(\mathrm{LCS}\) 长为 \(0\sim|S|\) 的方案数。

\(\mathrm{LCS}\) 是一个经典 DP,设 \(f_{i,j}\) 为前 \(i\) 位与给定字符串 \(S\)\(j\) 位的 \(\mathrm{LCS}\),转移方程:

\[f_{i,j}=\max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}+[T_i=S_j]) \]

发现关于 \(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_i=1+\sum_{(j,i)\in E,a_i\neq a_j} f_j \]

取模之后 \(f\) 数组状态也有 \(2^{50}\),很难像上一题操作,然而判断结果是否合法时是对 \(f\) 求一个前缀和,且连边并没有特殊要求,所以并不关心 \(f\) 的具体样子。

好路径为偶数的点连出的边是任意连的,好路径为奇数的点连出的边有奇偶性的限制,除此之外还有一个颜色的限制(同色不计入结果,因此同色任意连),于是只需要维护前 \(i\) 个点中有 \(j\) 个好路径为奇数的白点,\(k\) 个好路径为奇数的黑点,\(l\) 个好路径为偶数的白点,剩余的 \(i-j-k-l\) 个点是好路径为偶数的黑点,这样复杂度是 \(O(n^4)\)

好路径为偶数的点对任意的颜色都是没有影响的,所以最后一维也可以略去,复杂度 \(O(n^3)\)

所以 DP 套 DP 并不需要建出 DFA,只是需要在外层 DP 状态设计中表示出内层的当前状态。

参考资料

posted @ 2023-02-19 14:28  SoyTony  阅读(522)  评论(0编辑  收藏  举报