【POJ No. 2778】DNA 序列 DNA Sequence
北大OJ 题目地址
【题意】
DNA序列是一个只包含A、C、T和G的序列。分析DNA序列片段非常有用,若动物的DNA序列包含片段ATC,则意味着该动物可能患有遗传病。
给定m 个遗传病片段,求有多少种长度为n 的DNA序列不包含这些片段。
【输入输出】
输入:
第1行包含两个整数m (0≤m ≤10)和n (1≤n ≤2×109)。m 是遗传病片段的数量,n 是序列的长度。接下来的M 行,每行都包含一个DNA遗传病片段(长度不大于10)。
输出:
一个整数,不包含遗传病的DNA序列数mod 100000。
【样例】
【思路分析】
DNA序列只包含A、G、C、T共4种字母,给定m 个DNA遗传病片段,求有多少长度为n 的DNA序列不包含遗传病片段,可采用AC自动机解决。
【算法设计】
① 将遗传病片段插入字典树中。
② 构建AC自动机。注意:若当前节点的失败指针有结束标记,则对当前节点也要标记。
③ 构建邻接矩阵。对所有未标记的节点都重新编号,根据AC自动机构建邻接矩阵。
④ 求解矩阵的n 次幂,可用矩阵快速幂求解。
【举个栗子】
求解答案和矩阵有什么关系呢?
假设遗传病片段为{“ACG”, “C”},则将两个字符串插入字典树中并构建AC自动机。
从每个节点出发的边有4条(A、T、C、G)。
从状态0出发走1步有4种走法:①走A到状态1(安全);②走C到状态4(危险);③走T到状态0(安全);④走G到状态0(安全)。所以当n =1时,答案是3。
当n =2时,从状态0出发走2步,形成一个长度为2的字符串,只要在路径上没有经过危险节点,则有几种走法,答案就是几种。以此类推走n 步,就形成长度为n 的字符串。
这实际上相当于二元关系的复合运算,可以用图论里面的邻接矩阵相乘求解。
对上图的AC自动机建立邻接矩阵M :
2 1 0 0 1
2 1 1 0 0
1 1 0 1 1
2 1 0 0 1
2 1 0 0 1
其中,M[i , j ]表示从节点i 到j 只走1步有几种走法,M 的n 次幂表示从节点i 到j 走n 步有几种走法。
注意:要去掉危险节点的行和列。节点3和4是遗传病片段的结尾,是危险节点,节点2的失败指针指向4,当匹配“AC”时也就匹配了“C”,所以2也是危险节点。去掉危险节点2、3、4后,邻接矩阵变成M :
2 1
2 1
计算M[][]的n 次幂,∑(M[0, i ]) mod 100000就是答案。由于n很大,所以使用矩阵快速幂计算矩阵的n 次幂。
【算法实现】
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=105;
const int K=4;
const int MOD=100000;
struct mat{
int a[maxn][maxn];
mat(){
memset(a,0,sizeof(a));
}
};
int root,L;
mat mul(mat A,mat B){//矩阵乘法
mat C;
for(int i=0;i<L;i++)
for(int j=0;j<L;j++)
for(int k=0;k<L;k++)
C.a[i][j]=(C.a[i][j]+(long long)A.a[i][k]*B.a[k][j])%MOD;
return C;
}
mat pow(mat A,int n){//A^n
mat ans;
for(int i=0;i<L;i++)
ans.a[i][i]=1;//单位矩阵
while(n>0){
if(n&1)
ans=mul(ans,A);
A=mul(A,A);
n>>=1;
}
return ans;
}
struct ACAutomata{
int next[maxn][K],fail[maxn],end[maxn],id[maxn];
int idx(char ch){//转化数字
switch(ch){
case 'A':return 0;
case 'C':return 1;
case 'T':return 2;
case 'G':return 3;
}
return -1;
}
int newNode(){//新建结点
for(int i=0;i<K;i++)
next[L][i]=-1;
end[L]=0;
return L++;
}
void init(){//初始化
L=0;
root=newNode();
}
void insert(char s[]){//插入一个结点
int len=strlen(s);
int p=root;
for (int i=0;i<len;i++){
int ch=idx(s[i]);
if(next[p][ch]==-1)
next[p][ch]=newNode();
p=next[p][ch];
}
end[p]++;
}
void build(){//构建AC自动机
queue<int> Q;
fail[root]=root;
for(int i=0;i<K;i++){
if(next[root][i]==-1){
next[root][i]=root;
}
else{
fail[next[root][i]]=root;
Q.push(next[root][i]);
}
}
while(Q.size()){
int now=Q.front();
Q.pop();
if(end[fail[now]])
end[now]++;//重要!!如果当前结点的失败指针end有结束标记,当前结点的end++
for(int i=0;i<K;i++){
if(next[now][i]!=-1){
fail[next[now][i]]=next[fail[now]][i];
Q.push(next[now][i]);
}
else
next[now][i]=next[fail[now]][i];
}
}
}
int query(int n){
mat F;
int ids=0;
memset(id,-1,sizeof(id));
for(int i=0;i<L;i++)//对未标记的结点重新编号
if(!end[i])
id[i]=ids++;
for(int u=0;u<L;u++){
if(end[u]) continue;
for(int j=0;j<K;j++){
int v=next[u][j];
if(!end[v])
F.a[id[u]][id[v]]++;
}
}
L=ids;
F=pow(F,n);
int res=0;
for(int i=0;i<L;i++)
res=(res+F.a[0][i])%MOD;
return res;
}
}ac;
int main(){
int m,n;
char str[20];
while(~scanf("%d%d",&m,&n)){
ac.init();
while (m--){
scanf("%s",str);
ac.insert(str);
}
ac.build();
printf("%d\n",ac.query(n));
}
return 0;
}