首先介绍Trie(前缀树)这种数据结构。
举例来说比较简单
如图所示就是表示字符串集合{inn,int,tca,ten,to}的前缀树,建树的代码为
C++代码:
struct Trie{
int ch[maxnnode][sigma_size];//节点i的字符标号为j的那个子节点的编号
int val[maxnode];//记录字符串的值,通常为其标号
int sz;//节点个数
init(){sz=1;memset(ch[0],0,sizeof(ch[0]));}
int idx(char c){return c-'a';}
void insert(char *s,int v){
int u=0;int n=strlen(s);
for(int i=0;i<n;i++){
c=idx(s[i]);
if(!ch[u][c]){
memset(ch[sz],0,sizeof(ch[sz]));
val[sz]=0;//中间节点值为0
ch[u][c]=sz++;
}
u=ch[u][c];//往下走
}
val[u]=v;//字符串末尾节点记录值
}
void find(char *T){
int u=0;int n=strlen(T);
for(int i=0;i<n;i++){
int c=idx(T[i]);
if(!ch[u][c])break;//没找到
u=ch[u][c];
if(val[u])//找到了,这里写相应的操作
}
}
}
接下来介绍KMP算法
对已一个字符串T,我们有一个模板k,任务是找出T中是否有连续的字串刚好是K,显然暴力的方法最坏的复杂度为n*k,n为T的长度,k为K的长度,当模板或者T很长的时候,算法的效率显然是不高的,那么如何解决呢?熟悉自动机理论的话我们就可以知道,当输入的字符串失配的时候,我们会转到另外一个状态,这个状态并非一定是初始的没有任何字符输入的状态,图解的话更容易理解:
如图所示(找不到更好的图了)状态P5在失配的时候,T的下一个字符和K的下一个字符不匹配,那么忧郁前面已经有aba三个字符匹配,那么当前状态就会转移到三个字符匹配的状态,从而减少匹配的次数。
代码实现如下(可能和图解说的不一致,但是大体意思相近)
void
void getFail(char* P,int* f){//初始化失配函数
int m=strlen(P);
f[0]=0;f[1]=0;
for(int i=1;i<m;i++){
int j=f[i];
while(j&&P[i]!=P[j])j=f[j];
f[i+1]=(P[i]==P[j]?j+1:0);
}
}
void find(char* T,char* P,int* f){
int n=strlen(T);int m=strlen(P);
getFail(P,f);
int j=0;//当前节点编号
for(int i=0;i<n;i++){
while(j&&P[j]!=T[i])j=f[j];//顺着失配边走,直至找到匹配
if(P[j]==T[i])j++;
if(j==m)printf("%d\n",i-m+1,);//找到了
}
}
当有多个模板用来匹配的时候,KMP算法就已经不能再使用了,这时候就要用到AC自动机,原理还是和上述一样,找出失配函数,然后顺着失配边一直走,直至找到匹配,然后判断匹配是否是模板全部被匹配了,需要注意的一点是当找到一个模板后,应再顺着失配指针往回走,看看有没有其他串,如模板有101,01,当找到101时其实我们也找到了01,为了处理这种情况,我们增设一个last[j]的指针,表示节点j沿着失配指针往回走时遇到的下一个单词节点编号(这101失配是往回走会走到01的状态)。AC自动机的实现利用了Trie这个数据结构,具体见代码:
struct AhoCorasickAutomata{
int ch[maxn][sigma_size];
int f[maxn];
int cnt[maxnode];
int val[maxn];
int last[maxn];
int sz;
void init(){
sz=1;
memset(ch[0],0,sizeof(ch[0]));
memset(cnt,0,sizeof(cnt));
ms.clear();
}
int idx(char c){return c-'a';}
void insert(const char *s,int v){
int u=0;int n=strlen(s);
for(int i=0;i<n;i++){
int c=idx(s[i]);
if(!ch[u][c]){
val[sz]=0;
memset(ch[sz],0,sizeof(ch[sz]));
ch[u][c]=sz++;
}
u=ch[u][c];
}
val[u]=v;
ms[string(s)]=v;
}
void print(int j){
if(j){
cnt[val[j]]++;
print(last[j]);
}
}
void getFail(){
queue<int> q;
f[0]=0;
for(int c=0;c<sigma_size;c++){//初始化队列
int u=ch[0][c];
if(u){f[u]=0;q.push(u);last[u]=0;}
}
while(!q.empty()){
int r=q.front();q.pop();
for(int c=0;c<sigma_size;c++){
int u=ch[r][c];
if(!u)continue;
q.push(u);
int v=f[r];
while(v&&!ch[v][c])v=f[v];
f[u]=ch[v][c];
last[u]=val[f[u]]?f[u]:last[f[u]];//last[j]表示节点j沿着失配指针往回走时遇到的下一个节点编号
}
}
}
void find(char *T){
int n=strlen(T);
int j=0;//当前节点编号
for(int i=0;i<n;i++){
int c=idx(T[i]);
while(j&&!ch[j][c])j=f[j];
j=ch[j][c];
if(val[j])print(j);
else if(last[j])print(last[j]);
}
}
};
注意到一点是在getFail()函数中有一句话if(!u)continue;,我们将他改成if(!u){ch[r][c]=ch[f[r]][c];continue;}即把不存在的边都补上,对所有的转移都一视同仁,那么find函数中的while循环就是不必要的了