@xiaoziyao
        
        2021-01-09T11:53:23.000000Z
        字数 2123
        阅读 1489
    解题报告
CF86C Genetic engineering解题报告:
一道较套路的AC自动机dp题,但我没想出来/kk。
考虑先对所有给出的字符串建出AC自动机。
套路地设状态为在AC自动机上匹配到了位置,且已经用了个字符的方案总数。
我们发现在AC自动机上转移边分为trie树上原有的转移边和AC自动机添加的转移边,考虑直接在上面dp,可以正常进行trie树边的转移,对于AC自动机的转移,我们能转移当且仅当当前结点是一个字符串结束位置。
上面的转移方式乍一看好像没错,因为转移AC自动机添加的转移边时需要跳fail边,变成原来匹配串的后缀,因此会让一些没有覆盖的点无法覆盖。
但是我们考虑一种情况:先通过trie树上的匹配到了一个结束位置,然后往下跳一次trie树边,并跳一次AC自动机添加的转移边,最后再跳一次到了另一个位置较深的结束位置。
这样这个结束位置的长度足以覆盖前面没有覆盖的内容,这种方式仍然是合法的。
也就是说,我们跳fail边时,可能去除的前缀已经覆盖过了,因此这样的状态是不能转移的。
考虑另一种状态设计方式:表示在AC自动机上匹配到了位置,用了个字符,且后个字符还没有覆盖的方案数。
我们定义一个辅助数组,表示到了AC自动机上的位置,以为结尾的最长字符串长度为多少。
这样的转移也非常清晰:我们当前的状态为,通过AC自动机的转移边(无论是trie树边还是AC自动机新添加的转移边)转移到了点,那么我们有:
时间复杂度:。
#include<stdio.h>#include<iostream>#include<queue>using namespace std;const int maxn=105,maxk=4,maxl=12,mod=1000000009,maxm=1005;int n,m,tot,ans,maxlen;int chd[maxn][maxk],fail[maxn],f[maxm][maxl][maxn],dep[maxn],len[maxn];string s;queue<int>q;inline int get(char c){return c=='A'? 0:(c=='C'? 1:(c=='G'? 2:3));}void insert(string s){int now=1;for(int i=0;i<s.size();now=chd[now][get(s[i])],i++)if(chd[now][get(s[i])]==0)chd[now][get(s[i])]=++tot,dep[tot]=dep[now]+1;len[now]=dep[now];}void getfail(){while(!q.empty())q.pop();for(int i=0;i<=3;i++){if(chd[1][i])q.push(chd[1][i]),fail[chd[1][i]]=1;else chd[1][i]=1;}while(!q.empty()){int x=q.front();q.pop();for(int i=0;i<=3;i++){if(chd[x][i])q.push(chd[x][i]),fail[chd[x][i]]=chd[fail[x]][i];else chd[x][i]=chd[fail[x]][i];}len[x]=max(len[x],len[fail[x]]);}}void dp(){f[0][0][1]=1;for(int i=0;i<m;i++)for(int j=0;j<=maxlen;j++)for(int k=1;k<=tot;k++)if(f[i][j][k])for(int p=0;p<=3;p++){int x=chd[k][p];if(len[x]>j)f[i+1][0][x]=(f[i+1][0][x]+f[i][j][k])%mod;else f[i+1][j+1][x]=(f[i+1][j+1][x]+f[i][j][k])%mod;}}int main(){tot=1;scanf("%d%d",&m,&n);for(int i=1;i<=n;i++){cin>>s,maxlen=max(maxlen,(int)s.size());insert(s);}getfail();dp();for(int i=1;i<=tot;i++)ans=(ans+f[m][0][i])%mod;printf("%d\n",ans);return 0;}
