AC自动机

AC自动机

首先AC自动机总共需要要有三个前置思想:

  1. bfs搜索
  2. kmp匹配,其中主要是用到了next数组
  3. trie树(字典树)

有了这三个前置算法我们就可以开始学AC自动机了,AC自动机的本质是在一个匹配串中来找多个模式串,而kmp算法是一对一的那种,而AC自动机是一对多。然后AC自动机中的多个模式串是放在字典树中的。

 

1 struct node{
2     int next[26];
3     int fail;
4     int cnt;
5 };
6 
7 vector<node> trie;
8 int index = 0;

 

AC自动机中的框架是trie树,树中每个结点有三个东西,next【26】,fail, cnt :

  • next数组是指这个字符后面的字符,0代表a,1代表b,以此类推
  • faii是一个指针,假如这个结点是u(比如说这个u结点是‘a’字母),它的父亲结点f,然后这个f的fail结点指向的是ff  
    1. 如果说ff这个结点后面的字母有‘a’这个结点g,那么刚刚u结点的fail指向的是就是这个g
    2. 如果是这个ff结点后面中没有字母是‘a’这个结点,那么就从ff这个结点继续找fail,然后重复上面两个
    3. 如果说是一直都没有,那么最后就让u这个结点指向root
  • cnt涉及我们算一个匹配串中有几个模式串的问题,因为比如说这个模式串可能有好几个相同的,加的时候要一次性加,这个还涉及到后面的一个优化。

然后这个AC自动机要包含几个函数操作,首先pre_fail函数,在pre_fail中的用的是bfs来完成fail操作的,然后有insert来将模式串插入到树中,query是来查询匹配串的

 


 

 1 void insert(string str)//这个是一个正常的将字符串插入到trie树中 
 2 {
 3     int p = 0;
 4     for(int i = 0; i < str.size(); i++)
 5     {
 6         int v = str[i] - 'a';
 7         if(!trie[p].next[v]) 
 8         {
 9             trie[p].next[v] = ++index;
10         }
11         p = trie[p].next[v];
12     }
13     trie[p].cnt++;
14 }

这里insert 就是将这多个模式串插入到trie树中,如果学过了trie就知道了

 


 

 1 void fail_pre()
 2 {
 3     queue<int> q;
 4     for(int i = 0; i < 26; i++)//首先开始的时候root是0结点,是空的,它的子儿子都是指向的是0 
 5     {
 6         if(trie[0].next[i])
 7         {
 8             int son = trie[0].next[i];
 9             trie[son].fail = 0;
10             q.push(son);
11         }
12     }
13     while(q.size())//开始bfs搜索构建fail 
14     {
15         int f = q.front();
16         q.pop();
17         for(int i = 0; i < 26; i++)
18         {
19             if(trie[f].next[i])//如果说这个f点的后面的a+i这个字母存在,然后就进去 
20             {
21                 int now = trie[f].next[i]; 
22                 int ffail = trie[f].fail;
23                 while(~ffail && !trie[ffail].next[i])//如果说ffail这个点不是root(0)结点,并且这个点后面i+'a'这个字母也不存在,就继续顺着ffail往上搜索 
24                     ffail = trie[ffail].fail;        
25                 if(~ffail) trie[now].fail = trie[ffail].next[i];//如果说这个点后面的fail指向ffail这个结点 
26                 else trie[now].fail = 0;//如果是考~fail的结点跳出来,说明fail是-1,也就说明这个点是root点 
27                 q.push(now);//bfs正常操作 
28             }
29         }
30     }
31 }

这里构建的是每个结点fail指针,首先这里我们要默认两件事情,学过trie树的时候我们一定知道trie的root是一个空的结点,它不包含字符。

然后root后面的儿子的fail都是指向的是root,所以这些儿子的fail是0,而root的fail的是-1。

首先我们要清楚fail的真实作用是什么:

  • fail它的作用是就是在匹配串中先找到长的模式串后,靠着fail在长的模式串中找到短的模式串。
  • kmp中next找的是一个模式串中前后相同缀,而AC自动机找的是一个最长后缀,比如现在有三个模式串this,his,is然后此时this中的s的fail指向的是his中的s,然后his中的s的fail指向的是is中的s,从这组样例中我们可以看出:其实AC自动机靠的是顺着fail来找模式串的,如果从为什么要找最长的,因为这样才不会遗漏,如果this的s直接指向is的s,那么his这个模式串就会被漏掉

 

  1. 首先我们先将这个root儿子的fail开始指向root,然后呢加入到队列中开始bfs
  2. 接下来构建fail就会遇到三种情况:当下这个点是u,u点的fail是uf,u点的父亲是f,f点的fail点是ff,ff点的儿子的点我们叫做ffs
    1. 首先如果说是ffs是存在的,我们就让uf赋值为ffs
    2. 如果说ffs不存在,那么就继续靠着fail往上搜索,为什么这样子,因为fail指的是一个最长后缀,比如有abcde, abcd,cde 三个模式串,然后呢此时abcde的d指向的是abcd的d,而abcde中的e指的肯定是cde中的e
    3. 如果说都没有,那么这个uf指的就是root结点
  3. 将u这点继续压入到队列中,进行继续bfs

 

为什么不用dfs而用bfs:

因为fail它构架的时候需要用到上面的点的那些fail。


 

 1 int query(string str)//str是一个匹配串 
 2 {
 3     int ans = 0;
 4     int p = 0;
 5     for(int i = 0; i < str.size(); i++)
 6     {
 7         int v = str[i]-'a';
 8         while(!trie[p].next[v] && ~trie[p].fail) //如果这个p点的儿子没有,这个p点的fail也存在 
 9             p = trie[p].fail; //用p从上fail搜索,因为我们要从这里找到末尾是str【i】的模式串 
10         if(trie[p].next[v])     //
11             p = trie[p].next[v];
12         else continue;
13         int p2 = p;
14         while(~trie[p2].fail)
15         {
16             ans += trie[p2].cnt;
17             p2 = trie[p2].fail; 
18         }
19     }
20     return ans;
21 }

这里我们匹配的时候也是三个情况:

  1. 如果这个结点的儿子存在,就进入
  2. 如果这个结点儿子不存在,那么就跳fail
  3. 如果这个结点的儿子不存在的,fail也是-1,那么就下一个(此时在root)

注意:

  第一个while是在比如说我此时在haer模式串中的r,然后呢这里还有一个模式串aera,为了简洁不用再匹配ae,我直接跳fail来比对aera中的a,同时它是再一个模式串中尾部才会开始的

  第二个while不同,它是每一次都会的,因为比如说匹配串是hersa,有两个模式串hersa和er,那开始进入的是hersa中的分支中,如果值用fail再叶子结点进行追溯的话,那么就会把er给漏掉,所以每个位置都要进行检查。


 

完整模板:

  1 #include<iostream>
  2 #include<string>
  3 #include<vector>
  4 #include<queue>
  5 #include<cstring>
  6 using namespace std;
  7 
  8 struct node{
  9     int next[26];
 10     int fail;
 11     int cnt;
 12 };
 13 
 14 vector<node> trie;
 15 int index = 0;
 16 
 17 void insert(string str)//这个是一个正常的将字符串插入到trie树中 
 18 {
 19     int p = 0;
 20     for(int i = 0; i < str.size(); i++)
 21     {
 22         int v = str[i] - 'a';
 23         if(!trie[p].next[v]) 
 24         {
 25             trie[p].next[v] = ++index;
 26         }
 27         p = trie[p].next[v];
 28     }
 29     trie[p].cnt++;
 30 }
 31 
 32 void fail_pre()
 33 {
 34     queue<int> q;
 35     for(int i = 0; i < 26; i++)//首先开始的时候root是0结点,是空的,它的子儿子都是指向的是0 
 36     {
 37         if(trie[0].next[i])
 38         {
 39             int son = trie[0].next[i];
 40             trie[son].fail = 0;
 41             q.push(son);
 42         }
 43     }
 44     while(q.size())//开始bfs搜索构建fail 
 45     {
 46         int f = q.front();
 47         q.pop();
 48         for(int i = 0; i < 26; i++)
 49         {
 50             if(trie[f].next[i])//如果说这个f点的后面的a+i这个字母存在,然后就进去 
 51             {
 52                 int now = trie[f].next[i]; 
 53                 int ffail = trie[f].fail;
 54                 while(~ffail && !trie[ffail].next[i])//如果说ffail这个点不是root(0)结点,并且这个点后面i+'a'这个字母也不存在,就继续顺着ffail往上搜索 
 55                     ffail = trie[ffail].fail;        
 56                 if(~ffail) trie[now].fail = trie[ffail].next[i];//如果说这个点后面的fail指向ffail这个结点 
 57                 else trie[now].fail = 0;//如果是考~fail的结点跳出来,说明fail是-1,也就说明这个点是root点 
 58                 q.push(now);//bfs正常操作 
 59             }
 60         }
 61     }
 62 }
 63 
 64 int query(string str)//str是一个匹配串 
 65 {
 66     int ans = 0;
 67     int p = 0;
 68     for(int i = 0; i < str.size(); i++)
 69     {
 70         int v = str[i]-'a';
 71         while(!trie[p].next[v] && ~trie[p].fail) //如果这个p点的儿子没有,这个p点的fail也存在,这里就是在长串中找短串 
 72             p = trie[p].fail; //用p从上fail搜索,因为我们要从这里找到末尾是str【i】的模式串 
 73         if(trie[p].next[v])     //如果有就继续找 
 74             p = trie[p].next[v]; 
 75         else continue;        //如果都没有就搜索下一个        
 76         int p2 = p;            //从这里开始也是在做一个长串中找短串的一个操作 
 77         while(~trie[p2].fail) 
 78         {
 79             ans += trie[p2].cnt;
 80             p2 = trie[p2].fail; 
 81         }
 82     }
 83     return ans;
 84 }
 85 
 86 int main()
 87 {
 88     trie.resize(50);
 89     for(int i = 0; i < 50; i++)
 90     {
 91         for(int z = 0; z < 26; z++)
 92         {
 93             trie[i].next[z] = 0;
 94         }
 95         trie[i].fail = -1;
 96         trie[i].cnt = 0;
 97     }
 98         
 99     insert("she");
100     insert("h");
101     insert("her");
102     insert("his");
103     insert("this");
104     insert("is");
105     fail_pre();
106     cout << query("sherthis") << endl;
107     return 0;    
108 } 

(未完待续......)

 上下大佬的讲解地址:https://www.bilibili.com/video/BV1Nk4y1B7SL?from=search&seid=3215099446122167787

posted @ 2020-08-15 15:47  斌斌翻水水  阅读(162)  评论(0)    收藏  举报