返回顶部

AC自动机

模板题:HDU2222

AC自动机将KMP算法与trie树(字典树)相互结合

其主要处理多模匹配(即给定n个单词,再加一篇文章母串,在母串中查找这些单词)

如果是KMP算法,就会让每个字串与母串进行匹配,时间复杂度太高,所以用AC自动机。

在AC自动机算法中,主要步骤为:

                     1:构建字典树;

                     2:对每个结点建立失配指针;

                     3:模式匹配;

而失配指针则是AC自动机的关键所在

              失配指针指向的节点代表的字符串是当前节点代表的字符串的后缀。

              所以,当已经知道某个节点的失配指针所指向的节点时,那么其字节点的失配指针所指向的则为与该节点代表字符相同的,该节点父亲节点失配指针所指向的节点的字节点。(有点绕)

              即:对于跳转位置的选择,基于以下两点:

(1)对于根节点的所有子节点,它们的 fail 指针都指向根节点;

(2)对于其它节点,不妨设该节点为 u(对应字符为“ch”),沿着它的父节点

的 fail指针走,直到走到一个节点 v,其对应字符也为“ch”,然后把节点 u 的

fail 指针指向节点 v。如果一直走到根节点都没找到,则把 fail 指针指向根

点。

显然这个 v 节点的深度是小于 u 节点的,因此我们可以通过按节点的深度大小,也就是

BFS 的顺序来构建失配指针,构建过程与 KMP 类似,KMP 中 i 的 next 是沿着 i-1 的 next 不停往前跳来求,而 AC 自动机中 u 的 fail 则通过父节点的 fail 不停往上跳,直到找到一个节点它拥有对应字符的转移边为止来求得。

1:字典树

 1 void insert(char *s) //构建 Trie 树
 2 {
 3     int u=0;
 4      int n=strlen(s);
 5      for(int i=0;i<n;i++)
 6      {
 7          int c=s[i]-'a'; //把 a..z 转为 0..25
 8          if(!trie[u][c])
 9          {
10              memset(trie[tot],0,sizeof(trie[tot]));
11              val[tot]=0;
12              trie[u][c]=tot++;
13          }
14          u=trie[u][c];
15      }
16      val[u]++; //val[u]表示节点 0~节点 u 构成的单词数量
17 }

2:失配指针

 1 void getfail()
 2 {
 3     queue<int> q;
 4     fail[0]=0;
 5     int u=0;
 6     for(int i=0;i<26;i++)
 7     {
 8         u=trie[0][i];
 9         if(u)
10         {
11             q.push(u);
12             fail[u]=0;
13             last[u]=0;
14         }
15     }//最先初始,即根节点的字节点的失配指针都指向根 
16     while(!q.empty())
17     {
18         int r=q.front();q.pop();
19         for(int i=0;i<26;i++)
20         {
21             u=trie[r][i];
22             if(!u)//不存在,则跳向其父节点 r 前缀指针指向的第 i 个子节点 
23             {
24                 trie[r][i]=trie[fail[r]][i];
25                 continue; 
26             }
27             q.push(u);
28             int v=fail[r];
29             while(v&&!trie[v][i]) v=fail[v];//v存在并且 v没有第i个子节点,则跳到 v的前缀指针指向的节点
30             fail[u]=trie[v][i];
31             last[u]=val[fail[u]]?fail[u]:last[fail[u]];             
32         }
33     }
34 }

3:模式匹配

 1 int find(char *s)
 2 {
 3     int u=0,cnt=0;
 4     int lens=strlen(s);
 5     for(int i=0;i<lens;i++)
 6     {
 7         int c=s[i]-'a';
 8         u=trie[u][c];
 9         int temp=0;
10         if(val[u])    //如果u是一个完整单词
11             temp=u;
12         else if(last[u])
13                 temp=last[u];
14         while(temp)
15         {
16             cnt+=val[temp];
17             val[temp]=0;
18             temp=last[temp];
19         } 
20     }
21     return cnt;
22 }

最终代码:

  1 #include<bits/stdc++.h>
  2 using namespace std;
  3 const int maxn=1000010;
  4 const int maxm=50*10010;
  5 char t[60],s[maxn];
  6 int n;
  7 int trie[maxm][26];
  8 int val[maxm];//每个字符串的结尾节点都有一个非零的ed
  9 int fail[maxm];//失配指针
 10 int last[maxm];//last[i]=j,j节点表示的单词是i节点单词的后缀,且j节点是单词节点 
 11 int tot;
 12 void insert(char *s) //构建 Trie 树
 13 {
 14     int u=0;
 15      int n=strlen(s);
 16      for(int i=0;i<n;i++)
 17      {
 18          int c=s[i]-'a'; //把 a..z 转为 0..25
 19          if(!trie[u][c])
 20          {
 21              memset(trie[tot],0,sizeof(trie[tot]));
 22              val[tot]=0;
 23              trie[u][c]=tot++;
 24          }
 25          u=trie[u][c];
 26      }
 27      val[u]++; //val[u]表示节点 0~节点 u 构成的单词数量
 28 }
 29 void getfail()
 30 {
 31     queue<int> q;
 32     fail[0]=0;
 33     int u=0;
 34     for(int i=0;i<26;i++)
 35     {
 36         u=trie[0][i];
 37         if(u)
 38         {
 39             q.push(u);
 40             fail[u]=0;
 41             last[u]=0;
 42         }
 43     }//最先初始,即根节点的字节点的失配指针都指向根 
 44     while(!q.empty())
 45     {
 46         int r=q.front();q.pop();
 47         for(int i=0;i<26;i++)
 48         {
 49             u=trie[r][i];
 50             if(!u)//不存在,则跳向其父节点 r 前缀指针指向的第 i 个子节点 
 51             {
 52                 trie[r][i]=trie[fail[r]][i];
 53                 continue; 
 54             }
 55             q.push(u);
 56             int v=fail[r];
 57             while(v&&!trie[v][i]) v=fail[v];//v存在并且 v没有第i个子节点,则跳到 v的前缀指针指向的节点
 58             fail[u]=trie[v][i];
 59             last[u]=val[fail[u]]?fail[u]:last[fail[u]];             
 60         }
 61     }
 62 }
 63 int find(char *s)
 64 {
 65     int u=0,cnt=0;
 66     int lens=strlen(s);
 67     for(int i=0;i<lens;i++)
 68     {
 69         int c=s[i]-'a';
 70         u=trie[u][c];
 71         int temp=0;
 72         if(val[u])    //如果u是一个完整单词
 73             temp=u;
 74         else if(last[u])
 75                 temp=last[u];
 76         while(temp)
 77         {
 78             cnt+=val[temp];
 79             val[temp]=0;
 80             temp=last[temp];
 81         } 
 82     }
 83     return cnt;
 84 }
 85 int main()
 86 {
 87     int T;
 88     cin>>T;
 89     while(T--)
 90     {
 91         memset(trie[0],0,sizeof(trie[0]));//初始化0节点所对的子节点
 92         memset(last,0,sizeof(last));
 93         tot=1;
 94         cin>>n;
 95         for(int i=1;i<=n;i++)
 96         {
 97             scanf("%s",&t);
 98             insert(t);
 99         }
100         getfail();
101         scanf("%s",s);
102         int ans=find(s);
103         cout<<ans<<endl; 
104     }
105     return 0;
106 }
View Code
posted @ 2021-10-31 16:06  gyc#66ccff  阅读(69)  评论(0编辑  收藏  举报