zoukankan      html  css  js  c++  java
  • KMP AC自动机算法

     在KMP算法中,为了确定在匹配不成功时,下次匹配时j的位置,引入了next[]数组,next[j]的值表示P[0...j-1]中最长后缀的长度等于相同字符序列的前缀。

      对于next[]数组的定义如下:

     1) next[j] = -1  j = 0

     2) next[j] = max(k): 0<k<j   P[0...k-1]=P[j-k,j-1]

     3) next[j] = 0  其他

     如:

     P      a    b   a    b   a

     j      0    1   2    3   4

     next    -1   0   0    1   2

     即next[j]=k>0时,表示P[0...k-1]=P[j-k,j-1]

     因此KMP算法的思想就是:在匹配过程称,若发生不匹配的情况,如果next[j]>=0,则目标串的指针i不变,将模式串的指针j移动到next[j]的位置继续进行匹配;若next[j]=-1,则将i右移1位,并将j置0,继续进行比较。

     1 void getNext(char *p,int *next)
     2 {
     3     int i,j,temp;
     4     for(i=0;i<strlen(p);i++)
     5     {
     6         if(i==0)
     7         {
     8             next[i]=-1;     //next[0]=-1
     9         }
    10         else if(i==1) 
    11         {
    12             next[i]=0;      //next[1]=0
    13         }
    14         else
    15         {
    16             temp=i-1;
    17             for(j=temp;j>0;j--)
    18             {
    19                 if(equals(p,i,j))
    20                 {
    21                     next[i]=j;   //找到最大的k值
    22                     break;
    23                 }
    24             }
    25             if(j==0)
    26                 next[i]=0;
    27         }
    28     }
    29 }
    30 
    31 bool equals(char *p,int i,int j)     //判断p[0...j-1]与p[i-j...i-1]是否相等  
    32 {
    33     int k=0;
    34     int s=i-j;
    35     for(;k<=j-1&&s<=i-1;k++,s++)
    36     {
    37         if(p[k]!=p[s])
    38             return false;
    39     }
    40     return true;
    41 }
    非递归求next

    KMP 功能超找在主串中有多少个匹配串&&匹配串有多少个。

    代码KMP:匹配串查找主串中有多少个匹配串;//仅限一个匹配串 如多个匹配串应用AC自动机算法

     1 #include <cstdio>
     2 #include <cstring>
     3 #include <algorithm>
     4 using namespace std;
     5 
     6 char s[1000000];        //主串
     7 char p[100];        //模式串
     8 int next[100];      //模式串p在当前值不匹配时,返回到应该匹配的位置
     9 
    10 void getnext()      //找的模式串位置j在当前next回溯的位置
    11 {
    12     int j,k;
    13     next[0]=-1;
    14     j=0;k=-1;
    15     int len2=strlen(p);
    16     //p[len2]='#';
    17     
    18     while(j<=len2)
    19     {
    20         if(k==-1||p[j]==p[k])
    21         {
    22             j++;    k++;    
    23             next[j]=k;
    24             /* 优化后
    25             if(p]j]==p[k])      如当前位置和应该回溯的位置值相等的话,那么继续回溯;
    26                 next[j]=next[k];
    27             else
    28                 next[j]=k;      否则 直接等于k;
    29             */
    30         }
    31         else
    32             k=next[k];
    33     }
    34 }
    35 
    36 int main()
    37 {
    38     int t,i,j,c;
    39     scanf("%d",&t);
    40     while(t--)
    41     {
    42         c=0;
    43         scanf("%s",p);
    44         scanf("%s",s);
    45         i=0;j=0;
    46         int len1=strlen(s),len2=strlen(p);
    47         getnext();                  //得到next的值
    48         while(i<len1&&j<len2)                       //KMP入口;
    49         {
    50             if(j==-1||s[i]==p[j])                   //j==-1时时当前位置和p[0]不相等时,让指针都加+1的。
    51             {   
    52                 i++;j++;
    53             }
    54             else
    55                 j=next[j];
    56                 
    57              /*
    58              求返回匹配串在当前位置的的起始位置
    59              if(j>=len2)
    60                     return i-j;
    61              else
    62                 return -1;
    63              */
    64             if(j>=len2)                 //找匹配串在主串中有几个
    65             {
    66                 c++;    j=next[j];      //找到一个匹配的串之后。然后根据next求出他应该回溯到什么地方。
    67             }
    68         }                                       //KMP结束;
    69         printf("%d
    ",c);
    70     }
    71     return 0;
    72 }
    73                 
    KMP

    eg:  找出主串中有几个匹配串(aa 和aaa 有两个)

    样例输入 :

          abab

          ababab

    样例输出:

          2

    AC自动机算法:

    首先简要介绍一下AC自动机:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。

    如果你对KMP算法和了解的话,应该知道KMP算法中的next函数(shift函数或者fail函数)是干什么用的。KMP中我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符,当A[i+1]≠B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配,而next函数恰恰记录了这个j应该调整到的位置。同样AC自动机的失败指针具有同样的功能,也就是说当我们的模式串在Tire上进行匹配时,如果与当前节点的关键字不能继续匹配的时候,就应该去当前节点的失败指针所指向的节点继续进行匹配。

    AC自动机的构造:

    1.构造一棵Trie,作为AC自动机的搜索数据结构。

    2.构造fail指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配。如同 KMP算法一样, AC自动机在匹配时如果当前字符匹配失败,那么利用fail指针进行跳转。由此可知如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。

    3.扫描主串进行匹配。

    AC自动机详讲:

    我们给出5个单词,say,she,shr,he,her。给定字符串为yasherhs。问多少个单词在字符串中出现过。

    一、Trie

    首先我们需要建立一棵Trie。但是这棵Trie不是普通的Trie,而是带有一些特殊的性质。

    首先会有3个重要的指针,分别为p, p->fail, temp。

    1.指针p,指向当前匹配的字符。若p指向root,表示当前匹配的字符序列为空。(root是Trie入口,没有实际含义)。

    2.指针p->fail,p的失败指针,指向与字符p相同的结点,若没有,则指向root。

    3.指针temp,测试指针(自己命名的,容易理解!~),在建立fail指针时有寻找与p字符匹配的结点的作用,在扫描时作用最大,也最不好理解。

     

    对于Trie树中的一个节点,对应一个序列s[1...m]。此时,p指向字符s[m]。若在下一个字符处失配,即p->next[s[m+1]] == NULL,则由失配指针跳到另一个节点(p->fail)处,该节点对应的序列为s[i...m]。若继续失配,则序列依次跳转直到序列为空或出现匹配。在此过程中,p的值一直在变化,但是p对应节点的字符没有发生变化。在此过程中,我们观察可知,最终求得得序列s则为最长公共后缀。另外,由于这个序列是从root开始到某一节点,则说明这个序列有可能是某些序列的前缀。

    再次讨论p指针转移的意义。如果p指针在某一字符s[m+1]处失配(即p->next[s[m+1]] == NULL),则说明没有单词s[1...m+1]存在。此时,如果p的失配指针指向root,则说明当前序列的任意后缀不会是某个单词的前缀。如果p的失配指针不指向root,则说明序列s[i...m]是某一单词的前缀,于是跳转到p的失配指针,以s[i...m]为前缀继续匹配s[m+1]。

    对于已经得到的序列s[1...m],由于s[i...m]可能是某单词的后缀,s[1...j]可能是某单词的前缀,所以s[1...m]中可能会出现单词。此时,p指向已匹配的字符,不能动。于是,令temp = p,然后依次测试s[1...m], s[i...m]是否是单词。

    构造的Trie为:

    二、构造失败指针

    用BFS来构造失败指针,与KMP算法相似的思想。

    首先,root入队,第1次循环时处理与root相连的字符,也就是各个单词的第一个字符h和s,因为第一个字符不匹配需要重新匹配,所以第一个字符都指向root(root是Trie入口,没有实际含义)失败指针的指向对应下图中的(1),(2)两条虚线;第2次进入循环后,从队列中先弹出h,接下来p指向h节点的fail指针指向的节点,也就是root;p=p->fail也就是p=NULL说明匹配序列为空,则把节点e的fail指针指向root表示没有匹配序列,对应图-2中的(3),然后节点e进入队列;第3次循环时,弹出的第一个节点a的操作与上一步操作的节点e相同,把a的fail指针指向root,对应图-2中的(4),并入队;第4次进入循环时,弹出节点h(图中左边那个),这时操作略有不同。由于p->next[i]!=NULL(root有h这个儿子节点,图中右边那个),这样便把左边那个h节点的失败指针指向右边那个root的儿子节点h,对应图-2中的(5),然后h入队。以此类推:在循环结束后,所有的失败指针就是图-2中的这种形式。

    三、扫描

    构造好Trie和失败指针后,我们就可以对主串进行扫描了。这个过程和KMP算法很类似,但是也有一定的区别,主要是因为AC自动机处理的是多串模式,需要防止遗漏某个单词,所以引入temp指针。

    匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。

     对照上图,看一下模式匹配这个详细的流程,其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。

    //AC自动机模板
    
    const int kind=26;
    
    struct node {
        node *fail;     //失败指针
        node *next[kind];//Tire树每个节点的子节点个数
        int cot;        //是否为该单词的最后一个节点
        node()          //构造函数初始化
        {
            fail=NULL;
            cot=0;
            memset(next,0,sizeof(next));
        }
    }*q[500002];        //队列,方便用于bfs构造失败指针
    char str[1000001];  //模式串
    int head,tail;      //队列的头尾指针
    
    
    void inser(char *str,node *root)        //构造tire数;
    {
        node *p=root;
        int i=0,index;
        while(str[i])
        {
            index=str[i]-'a';
            if(p->next[index]==NULL)
                p->next[index]=new node();
            p=p->next[index];       //p指向i字符相等的节点
            i++;
        }
        p->cot++;       //在单词的最后一个节点是的cot加1,代表单词数加1;
    }
    
    void build_ac_automatio(node *root)         //写出fail的指向
    {
        int i;
        root->fail=NULL;
        q[head++]=root;             //bfs
        while(head!=tail)
        {
            node *temp=q[tail++];
            node *p=NULL;
            for(i=0;i<26;i++)
            {
                if(temp->next[i]!=NULL)         //当前的子节点i不为空
                {
                    if(temp==root)
                        temp->next[i]->fail=root;
                    else
                    {
                        p=temp->fail;               //记录一下当前节点失败节点指向那个节点
                        while(p!=NULL)              //直到找到
                        {
                            if(p->next[i]!=NULL)        //失败节点指向的节点的i子节点不为空的话,就和当前节点的i相等,即当前i的失败节点即为父节点失败节点指向的i子节点、
                            {
                                temp->next[i]->fail=p->next[i]; break;
                            }
                            p=p->fail;
                        }
                        if(p==NULL)
                            temp->next[i]->fail=root;
                    }
                   q[head++]=temp->next[i];         //先把失败节点指向,再进入队列
                }
            }
        }
    }
    
    
    int query(node *root)           //查询
    {
        int i=0,cnt=0,index,len=strlen(str);
        node *p=root;
        
        while(str[i])
        {
            index =str[i]-'a';
            while(p->next[index]==NULL&&p!=root)        //当前节点失败,&& 第root节点
                p=p->fail;
            p=p->next[index];
            p=(p==NULL)?root:p;
            node *temp=p;
            while(temp!=root&&temp->cot!=-1)        //查询当前字符节点是否为结束字符, && 单词之前没有
            {
                cnt+=temp->cot;
                if(temp->cot>0)
                    temp->cot=-1;
                temp=temp->fail;
            }
            i++;
        }
        return cnt;
    }
  • 相关阅读:
    三维偏序(陌上花开)
    【题解】洛谷P4735最大异或和
    【题解】CF#229 E-Gifts
    【题解】JXOI2017颜色
    【题解】SDOI2009Bill的挑战
    【题解】CF#960 H-Santa's Gift
    【题解】Atcoder ARC#85 E-MUL
    【题解】CF#1012 C-Hill
    【题解】CF#852 E-Casinos and travel
    【题解】Atcoder ARC#90 F-Number of Digits
  • 原文地址:https://www.cnblogs.com/WDKER/p/5212651.html
Copyright © 2011-2022 走看看