AC自动机算法

首先介绍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循环就是不必要的了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值