CSP-CCF2020(第20次) 第五题 解密密码本 题解(AC自动机+字典树+DP)

题意:

给n个单词,要你构造长度为k的密文,使得密文解密后是由这n个单词构成的
解密书,有k页,一开始在第1页,每页26行。第i行一个字符a和一个数字b表示密文i解码后变成a,并翻到第b页
并且构造的密文中的子串不能出现给出的单词。
问k=1,2,3…m时候的方案数
n <= 50, 单词总长度 <= 50, m <= 2000
给出的单词不会出现重复,也不会有一个是另一个的前缀。单词可以重复在明文中出现。

解题思路:

看一下限制条件:

  1. 长度要为k
  2. 密文中不可以出现任何子串为某个单词
  3. 明文是由给出的单词构成
  4. 密码书翻到的页数不同,密文对应的明文不同

对于第二个限制,相当于建立一棵AC自动机之后,拿密文在AC自动机上跑,不会跑到某个后缀为给出的单词的结点上去。

对于第三个限制,相当于用解密出来的明文在字典树上跑,每次跑的都是字典树上存在的边,并且走到某个单词的结尾就跳回根,最终停留在根上。

看到单词总长度<=50,意味着建树之后结点数不会超过50,所以用:
d p ( i , j , k , l ) dp(i,j,k,l) dp(i,j,k,l)来表示当前长度为i,当前密文在ac自动机上走到结点j,明文在字典树上走到k,密码书翻到了第l页的方案数。
转移显然。利用滚动数组降低空间复杂度为 O ( n ∣ S ∣ 2 ) O(n|S|^2) O(nS2),时间复杂度为 O ( m n ∣ S ∣ 2 ) O(mn|S|^2) O(mnS2)
考试的时候忘记调用build获取fail指针的我是笨蛋。
赛后调用build之后本地测试25个测试用例均在1s内得到正确答案。

#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int maxn = 105;
int ch[maxn][26], ed[maxn], fail[maxn], nxt[maxn][26], val[maxn];
int n,m;
#define P pair<int,int>
int g[55][26];//i th book got j,ned to full
int to[55][26];
char cmd[maxn];
int rt, tot;
void ins(char *s){
    int p = rt;
    while(*s){
        int x = *s - 'a';
        if(!ch[p][x]){
            ch[p][x] = ++tot;
            nxt[p][x] = tot;
        }
        p = ch[p][x];
        s++;
    }
    ed[p] = 1;
    val[p] = 1;
}
queue<int> q;
void build(){
    while(q.size()) q.pop();
    fail[rt] = rt;
    for(int i = 0; i < 26; ++i){
        if(nxt[rt][i]){
            fail[ch[rt][i]] = rt;
            q.push(ch[rt][i]);
        }
    }
    while(q.size()){
        int u = q.front(); q.pop();
        for(int i = 0; i < 26; ++i){
            if(!nxt[u][i]){
                nxt[u][i] = nxt[fail[u]][i];
            }else{
                fail[nxt[u][i]] = nxt[fail[u]][i];
                ed[nxt[u][i]] |= ed[nxt[fail[u]][i]];
                q.push(nxt[u][i]);
            }
        }
    }
    return;
}
int dp[2][55][55][55];//len is i, ac is j, tree is k, book is l

int main(){

    scanf("%d%d", &n, &m);
    for(int i = 0; i < 26; ++i){
        for(int k = 1; k <= n; ++k){
            char s[5];
            int x = 0;
            scanf("%s",s);
            int ch = s[0] - 'a';
            int len = strlen(s);
            for(int j = 1; j < len; ++j) x = x*10 + s[j] - '0';
            g[k][i] = ch;
            to[k][i] = x;
        }
    }
    rt = tot = 0;
    while(scanf( "%s", cmd)!=EOF){
        ins(cmd);
        //cout<<cmd<<endl;
    }
    build();//考试的时候忘记build了...

    //cout<<ch[0][0]<<endl;
    dp[0][0][0][1] = 1;
    int cur = 0, w = 1;
    for(int i = 0; i < m; ++i){
        int ans = 0;
        for(int j = 0; j <= tot; ++j){
            for(int k = 0; k <= tot; ++k){
                for(int l = 1; l <= n; ++l){
                    if(dp[cur][j][k][l] == 0) continue;
                    //cout<<"i:"<<i <<" j:"<<j<<" k:"<<k<<" l:"<<l<<" dp:"<<dp[cur][j][k][l]<<endl;
                    for(int x = 0; x < 26; ++x){
                        if(ed[nxt[j][x]]) continue;
                        int v = g[l][x];
                        //cout<<"x:"<<x<<" v:"<<v<<endl;
                        if(ch[k][v] == 0) continue;
                        //cout<<"j:"<<j<<" x:"<<x<<endl;
                        //cout<<"x:"<<char(v+'a')<<endl;
                        if(val[ch[k][v]] == 0){
                            dp[w][nxt[j][x]][ch[k][v]][to[l][x]] = (dp[w][nxt[j][x]][ch[k][v]][to[l][x]] + dp[cur][j][k][l])%mod;
                            //cout<<"x: "<<(char)(x+'a')<<" j:"<<nxt[j][x]<<" k:"<<ch[k][v]<<" l:"<<to[l][x]<<endl;
                        }
                        else {
                            dp[w][nxt[j][x]][0][to[l][x]] = (dp[w][nxt[j][x]][0][to[l][x]] + dp[cur][j][k][l])%mod;
                            //cout<<"x: "<<(char)(x+'a')<<" j:"<<nxt[j][x]<<" k:"<<0<<" l:"<<to[l][x]<<endl;
                            ans = (ans + dp[cur][j][k][l])%mod;
                        }

                    }
                    dp[cur][j][k][l] = 0;
                }
            }
        }
        swap(cur, w);
        printf("%d\n", ans);
        //if(ans != Ans[i+1]){printf("Wa!");return 0;}
    }
    //printf("Ac!\n");
    return 0;
}

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值