AC自动机
AC自动机
首先AC自动机总共需要要有三个前置思想:
- bfs搜索
- kmp匹配,其中主要是用到了next数组
- 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
- 如果说ff这个结点后面的字母有‘a’这个结点g,那么刚刚u结点的fail指向的是就是这个g
- 如果是这个ff结点后面中没有字母是‘a’这个结点,那么就从ff这个结点继续找fail,然后重复上面两个
- 如果说是一直都没有,那么最后就让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这个模式串就会被漏掉
- 首先我们先将这个root儿子的fail开始指向root,然后呢加入到队列中开始bfs 接下来构建fail就会遇到三种情况:当下这个点是u,u点的fail是uf,u点的父亲是f,f点的fail点是ff,ff点的儿子的点我们叫做ffs
- 首先如果说是ffs是存在的,我们就让uf赋值为ffs
- 如果说ffs不存在,那么就继续靠着fail往上搜索,为什么这样子,因为fail指的是一个最长后缀,比如有abcde, abcd,cde 三个模式串,然后呢此时abcde的d指向的是abcd的d,而abcde中的e指的肯定是cde中的e
- 如果说都没有,那么这个uf指的就是root结点
- 将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 }
这里我们匹配的时候也是三个情况:
- 如果这个结点的儿子存在,就进入
- 如果这个结点儿子不存在,那么就跳fail
- 如果这个结点的儿子不存在的,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

浙公网安备 33010602011771号