【学习笔记】DP 套 DP

Page Views Count

内外层 DP#

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

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

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

一道题#

Luogu-P4590 TJOI 2018 游园会

求与给定字符串的 LCS 长为 0|S| 的方案数。

LCS 是一个经典 DP,设 fi,j 为前 i 位与给定字符串 Sj 位的 LCS,转移方程:

fi,j=max(fi1,j,fi,j1,fi1,j1+[Ti=Sj])

发现关于 fi 的一行只需要已知 Ti 以及关于 fi1 的一行即可,且 fi 一行相邻两位只增加 01,可以把这个差分压成一个状态,还原成原数组后结合当前字符就可以得到下一行。

于是可以建出这个 DFA。

外层 DP 就是在 DFA 上进行的,设 DPi,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 写出来是:

fi=1+(j,i)E,aiajfj

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

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

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

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

参考资料#

作者:SoyTony

出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_DP_of_DP.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   SoyTony  阅读(1120)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示