Loading

dp 套 dp 学习笔记

所谓 dp 套 dp就是指这样一类题:给定 dp 结果,问有多少种输入能够导致这个结果,通常来说,这种问题可以转化为在自动机上 dp,内层 dp 的转移就是在自动机上转移。

这样说肯定是会有些笼统,我们考虑下面这一道例题。

例题

链接

如果是一个求 LCS 的 dp 的话,转移式是这样的:

\[f_{i,j}=\begin{cases} f_{i-1,j-1}+1(s_i=t_j)\\ \max\{ f_{i-1,j-1},f_{i-1,j},f_{i,j-1} \} \end{cases} \]

我们考虑 dp 的第二维只有 15 个,且相邻两项之间相差最多为 \(1\),所以我们可以设状态 \(F_{i,S}\) 表示另一个字符串长度为 \(i\) LCS 为 \(S\) 的方案数有哪些,我们只需要处理处每当我们往后加一个字符后 \(S\) 是怎么变化的就可以进行转移。具体看代码实现。

那么我们发现,这个东西内层的 \(S\to T\) 就相当于在自动机上转移,所以 dp 套 dp 的是指是在自动机上 dp。

代码:

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1010
#define M 40010
using namespace std;

const int INF=0x3f3f3f3f;
const int mod=1e9+7;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

template<typename T> inline T Max(T a,T b){return a<b?b:a;}

int n,m,t,lim,f[N],g[N],h[M][4],ans[N],F[N][M],b[N];
char s[N];

inline int ID(char c){
    if(c=='A') return 0;else if(c=='C') return 1;else if(c=='G') return 2;else if(c=='T') return 3;
    assert(0);return -1;
}

inline char InvID(int x){
    if(x==0) return 'A';else if(x==1) return 'C';else if(x==2) return 'G';else if(x==3) return 'T';
    assert(0);return -1;
}

inline void Init(){
    scanf("%s",s+1);read(m);n=strlen(s+1);lim=(1<<n)-1;
}

inline int TRANS(int S,char c){
    for(int i=0;i<=n-1;i++) f[i+1]=(S>>i)&1;
    for(int i=1;i<=n;i++) f[i]+=f[i-1];
    for(int i=1;i<=n;i++){
        if(s[i]==c){
            g[i]=f[i-1]+1;
        }
        else g[i]=Max(g[i-1],Max(f[i],f[i-1]));
    }
    for(int i=1;i<=n;i++) b[i]=g[i];
    for(int i=1;i<=n;i++) g[i]=g[i]-b[i-1];
    int now=0;
    for(int i=1;i<=n;i++) if(g[i]) now|=(1<<(i-1));
    return now;
}

inline void GetPre(){
    for(int i=0;i<=lim;i++){
        for(int j=0;j<=3;j++){
            h[i][j]=TRANS(i,InvID(j));
            // printf("h[%d][%d]=%d\n",i,j,h[i][j]);
        }
    }
}

inline void Solve(){
    F[0][0]=1;
    for(int i=0;i<=m-1;i++){
        for(int j=0;j<=lim;j++){
            // printf("F[%d][%d]=%d\n",i,j,F[i][j]);
            if(!F[i][j]) continue;
            for(int k=0;k<=3;k++){
                (F[i+1][h[j][k]]+=F[i][j])%=mod;
            }
        }
    }
    for(int i=0;i<=lim;i++){
        // printf("F[%d][%d]=%d\n",m,i,F[m][i]);
        (ans[__builtin_popcount(i)]+=F[m][i])%=mod;
    }
}

inline void Print(){
    for(int i=0;i<=n;i++){
        printf("%d\n",ans[i]);
    }
}

inline void Clear(){
    fill(ans,ans+n+1,0);
    memset(F,0,sizeof(F));
}

int main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(t);
    while(t--){
        Init();GetPre();Solve();Print();Clear();
    }
    return 0;
}

以上代码出现错误总结如下:

  1. g 忘记差分
  2. 变量名用混
  3. 忘记清空数组。

总结

我们考虑 dp 套 dp 这些题的通性,dp 套 dp 分为两个 dp:内层 dp 和外层 dp,在题目中,内层 dp 的转移通常作为自动机上的转移,而外层 dp 是我们的主题,通常运用状压差分等技巧简化状态。

posted @ 2021-11-07 16:25  hyl天梦  阅读(125)  评论(0编辑  收藏  举报