本博客由南昌理工学院ACM集训队赞助播出 教练要求
AC自动机
文字说明
本算法需要先掌握字典树和KMP算法后再来学习
如果没学过其中的任意一种,请先去学习再来
- 用我见过的大佬引用:AC自动机简单来说就是Tire Tree +
看毛片KMP,也就是在树上看毛片KMP - AC自动机用来解决多模式串匹配,也就是给好几个子串,一个很长很长的母串,让你处理一些问题,比如什么子串出现的次数或种类之类的。
先用字典树建立树,然后用fail指针将子串串起来,和KMP中的next数组很像,我们定义fail数组,fail[i]为与以i节点为结尾的串的后缀有最大公共长度的前缀的结尾编号。
是不是看不懂,我也是orz
还是看图吧
图片分析
首先对she,shr,say,her建立字典树,
对于以d结尾的子串she,不存在任何一个其他子串的前缀与she匹配,但he与的she后缀he匹配,这是匹配到的最长情况,于是这个e上的fail指针就指向he上的e。然后,如果没有找到任何一个前缀与当前串的任何一个后缀一样的话,那么fail只能指到根了。和KMP的原理一样,fail跳到的地方必然这个子串的前面不用再比较了,因为根据fail的定义,它的已经前缀已经被比较过并且匹配了。只用再往后比就行了
代码解析
// 建立字典树模板
void build() {
int len = tmp.length();
int u = 0;
for(int i = 0; i < len; ++i) {
int s = tmp[i] - 'a'; // 清晰一点这里先赋值字母
if(tr[u].ch[s] == 0)
tr[u].ch[s] = ++cnt;
u = tr[u].ch[s];
}
tr[u].num++; // 这里表示每一个子串结束
}
// 建立fail指针 用链表串起来
void get_fail() {
queue<int> q;
for(int i = 0; i < 26; ++i) {
if(tr[0].ch[i]) { // 找到有字目的点将fail指针指向根
tr[tr[0].ch[i]].fail = 0;
q.push(tr[0].ch[i]); // 进队
}
}
while(!q.empty()) { // 队列为不为空
int u = q.front();
q.pop();
for(int i = 0; i < 26; ++i) {
if(tr[u].ch[i]) { // 判断当前点的子树字母的点有某有
tr[tr[u].ch[i]].fail = tr[tr[u].fail].ch[i]; // 将当前子树点的fail赋给当前点fail指向点的同一个字母
q.push(tr[u].ch[i]); // 进队
} else {
tr[u].ch[i] = tr[tr[u].fail].ch[i];
// 某有 就让这个子树点直接指向当前点fail指向点的同一个字母
}
}
}
}
// 最后就是求啦
int ac() {
int len = tmp.length();
int u = 0, ans = 0;
for(int i = 0; i < len; ++i) { // 母串
int s = tmp[i]-'a'; // 清晰一点这里先赋值字母
u = tr[u].ch[s]; // 当前最长子串
int v = u;
while(v && tr[v].num != -1) { // 开始找每一个子串啦
ans += tr[v].num;
tr[v].num = -1;
// 题目要求求种类 加过了之后就不用再加了
// 如果题目要求的是数量 就不用赋值-1
v = tr[v].fail;
}
}
return ans;
}
完整模板代码
#include<iostream>
#include<queue>
#include<string>
#include<cstring>
using namespace std;
const int MAX =1000000 + 10;
int n, cnt;
string tmp;
struct node{
int fail,num;
int ch[26];
}tr[MAX];
void build() { // 这里是建立字典树
int len = tmp.length();
int u = 0;
for(int i = 0; i < len; ++i) {
int s = tmp[i] - 'a';
if(tr[u].ch[s] == 0)
tr[u].ch[s] = ++cnt;
u = tr[u].ch[s];
}
tr[u].num++;
}
void get_fail() { // 这里是fail指针 用链表串起来
queue<int> q;
for(int i = 0; i < 26; ++i) {
if(tr[0].ch[i]) {
tr[tr[0].ch[i]].fail = 0;
q.push(tr[0].ch[i]);
}
}
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = 0; i < 26; ++i) {
if(tr[u].ch[i]) {
tr[tr[u].ch[i]].fail = tr[tr[u].fail].ch[i];
q.push(tr[u].ch[i]);
} else {
tr[u].ch[i] = tr[tr[u].fail].ch[i];
}
}
}
}
int ac() {
int len = tmp.length();
int u = 0, ans = 0;
for(int i = 0; i < len; ++i) {
int s = tmp[i]-'a';
u = tr[u].ch[s];
int v = u;
while(v && tr[v].num != -1) {
ans += tr[v].num;
tr[v].num = -1;
// 题目要求求种类 加过了之后就不用再加了
// 如果题目要求的是数量 就不用赋值-1
v = tr[v].fail;
}
}
return ans;
}
int main() {
cin >> n;
for(int i = 0; i != n; ++i) {
cin >> tmp;
build();
}
tr[0].fail = 0;
get_fail();
cin >> tmp;
int ans = ac();
cout << ans << endl;
return 0;
}