AC自动机是KMP算法和Trie(字典树)的巧妙结合这篇文章主要讲针对几个例题给出解答模版(主要是知识点自己讲不清楚)。
至于针对的知识点,给上几个我认为说的比较好的传送门,读者可以自行选择阅读。(我就是看这几个大佬文章学的)
KMP : https://blog.csdn.net/v_JULY_v/article/details/7041827?spm=1001.2014.3001.5502
AC自动机原版 :
https://www.cnblogs.com/cmmdc/p/7337611.html
https://www.cnblogs.com/sclbgw7/p/9260756.html
AC自动机的last优化 :
https://www.cnblogs.com/sclbgw7/p/9875671.html
感谢以上大佬给出的解答%%%%。
下面我们针对几道模板例题进行解答。
P3808 【模板】AC 自动机(简单版)
https://www.luogu.com.cn/problem/P3808
本体是AC自动机的原始版,要求统计有多少模式串是文本串中出现过,代码如下
#include<bits/stdc++.h>
using namespace std;
//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i < (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))
typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL, LL> PLL;
const int MAXN = 1e7 + 1000;
const int INF = 0x3f3f3f3f;
const int SIZE = 26 + 10;
const LL LNF = 9223372036854775807;
int ch[MAXN][SIZE]; //用于储存字典树,代表一个字符的地址
bool vis[MAXN]; //最后计数时记录遍历访问情况
int val[MAXN]; //记录某模式串的结束结点
LL ans = 0;
int idx(char c){ return c - 'a'; };//把字幕'a' - 'z' 转换为 0 - 25
//建立字典树
struct Trie{
int pos; //新的存储结点的位置
Trie(){pos = 1; ms(ch[0], 0); ms(vis, false);};//创建新树的初始化
void insert(string T){//插入字典树中
int Tlen = T.length(), p = 0;
lop(i, 0, Tlen){
int x = idx(T[i]);
if(!ch[p][x]){
ms(ch[pos], 0);
val[pos] = 0;
ch[p][x] = pos++;
}
p = ch[p][x];
}
val[p]++;
}
};
//Fail指针(用数组表示)的建立与last优化
int last[MAXN], fail[MAXN];
void getFail(){
queue<int> q;
fail[0] = 0;//根只能指向自己
lop(x, 0, 26){//对于根直接相连的结点进行初始化
int u = ch[0][x];
if(u){//存在这个结点
fail[u] = 0;
last[u] = 0;
q.push(u);
}
}
//进入类似BFS环节
while(!q.empty()){
int u = q.front();
q.pop();
lop(x, 0, 26){
int v = ch[u][x]; //v是u的子结点
if(!v){//u没有这个子节点 last优化一
ch[u][x] = ch[fail[u]][x];//这个u直接连向能匹配到x的fail指针的子节点 连不到则回到0
continue;
}
q.push(v);//如果u有这个子结点,入队
int w = fail[u];
while(w && !ch[w][x]) w = fail[w];//更新fail[w]找到第一个子节点有v的点 last优化优化二
fail[v] = ch[w][x];
last[v] = val[fail[v]] ? fail[v] : last[fail[v]];
}
}
}
//正式AC自动机的查找过程
void CountNum(int j){//计数
if(j && !vis[j]){
ans += val[j];
vis[j] = true;
CountNum(last[j]);//如果j还能找到下一个结束节点,继续搜索
}
}
void ACautoFind(string S){//自动机
int Slen = S.length();
int now = 0;//当前所在位置
lop(i, 0, Slen){//遍历每个字符
int x = idx(S[i]);
now = ch[now][x];
if(val[now])//now是结束节点
CountNum(now);
else if(last[now]){//通过now的last指针可以连到一个结束节点
CountNum(last[now]);
}
}
}
int main(){
cin.tie(0);
cout.tie(0);
cin.sync_with_stdio(false);
int n;
string str;
Trie trie;
ans = 0;
cin >> n; //getchar() + getline(cin, str)过不了!!!
rep(i, 1, n){
cin >> str;
trie.insert(str);
}
getFail();
cin >> str;
ACautoFind(str);
cout << ans << el;
return 0;
}
P5357 【模板】AC 自动机(二次加强版)
https://www.luogu.com.cn/problem/P5357
作为上一题的加强版本,其实并没有过多的修改,原本因为统计个数所以设置了vis[]数组避免多次统计,而新的题目是要输出出现最多次的模式串,那么我们就应该统计出现次数,所以vis[]的含义从原来的是否出现变成了出现次数。而其次就是增加num[]数组用于标记每个结束结点对应的字符串编号(我们这里设为从1 ~ n)。在记录完以后再遍历输出即可,与第一版没太大差异,只需要改动输出和计数的细节。代码如下
#include<bits/stdc++.h>
using namespace std;
//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i < (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))
typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL, LL> PLL;
const int MAXN = 1e7 + 1000;
const int INF = 0x3f3f3f3f;
const int SIZE = 26 + 10;
const LL LNF = 9223372036854775807;
int ch[MAXN][SIZE]; //用于储存字典树,代表一个字符的地址
int val[MAXN]; //记录某模式串的结束结点
//多加一个处理个数的数组
int cnt = 0; //记录编号 从1开始
int num[MAXN]; //登记结束结点所对应的子串编号
int vis[MAXN];
LL ans = 0;
int idx(char c){ return c - 'a'; };//把字幕'a' - 'z' 转换为 0 - 25
//建立字典树
struct Trie{
int pos; //新的存储结点的位置
Trie(){ //创建新树的初始化
cnt = 0;
pos = 1;
ms(ch[0], 0);
ms(vis, 0);
ms(num, 0);
};
void insert(string T){//插入字典树中
int Tlen = T.length(), p = 0;
lop(i, 0, Tlen){
int x = idx(T[i]);
if(!ch[p][x]){
ms(ch[pos], 0);
val[pos] = 0;
ch[p][x] = pos++;
}
p = ch[p][x];
}
val[p]++;
num[p] = (++cnt);
}
};
//Fail指针(用数组表示)的建立与last优化
int last[MAXN], fail[MAXN];
void getFail(){
queue<int> q;
fail[0] = 0;//根只能指向自己
lop(x, 0, 26){//对于根直接相连的结点进行初始化
int u = ch[0][x];
if(u){//存在这个结点
fail[u] = 0;
last[u] = 0;
q.push(u);
}
}
//进入类似BFS环节
while(!q.empty()){
int u = q.front();
q.pop();
lop(x, 0, 26){
int v = ch[u][x]; //v是u的子结点
if(!v){//u没有这个子节点 last优化一
ch[u][x] = ch[fail[u]][x];//这个u直接连向能匹配到x的fail指针的子节点 连不到则回到0
continue;
}
q.push(v);//如果u有这个子结点,入队
int w = fail[u];
while(w && !ch[w][x]) w = fail[w];//更新fail[w]找到第一个子节点有v的点 last优化优化二
fail[v] = ch[w][x];
last[v] = val[fail[v]] ? fail[v] : last[fail[v]];
}
}
}
//正式AC自动机的查找过程
void CountNum(int j){//计数
if(j){
vis[num[j]]++;
CountNum(last[j]);//如果j还能找到下一个结束节点,继续搜索
}
}
void ACautoFind(string S){//自动机
int Slen = S.length();
int now = 0;//当前所在位置
lop(i, 0, Slen){//遍历每个字符
int x = idx(S[i]);
now = ch[now][x];
if(val[now])//now是结束节点
CountNum(now);
else if(last[now]){//通过now的last指针可以连到一个结束节点
CountNum(last[now]);
}
}
}
int main(){
cin.tie(0);
cout.tie(0);
cin.sync_with_stdio(false);
int n;
while(cin >> n && n){ //getchar() + getline(cin, str)过不了!!!
string str[200], T;
Trie trie;
ans = 0;
rep(i, 1, n){
cin >> str[i];
trie.insert(str[i]);
}
getFail();
cin >> T; //文本串
ACautoFind(T);
int maxnum = -1;
rep(i, 1, n){
maxnum = max(maxnum, vis[i]);
}
cout << maxnum << el;
rep(i, 1, n){
if(vis[i] == maxnum)
cout << str[i] << el;
}
}
return 0;
}
P5357 【模板】AC 自动机(二次加强版)
https://www.luogu.com.cn/problem/P5357
对于这第三个版本我开始看感觉好像比第二个还容易,甚至就直接每个输出就完事了,然后看了下数据,明显数据量加大了不少。于是用原本的代码提交了一发。结果肯定是过不了的(交的时候其实也就猜到了,不然怎么可能叫二次加强呢)
并且我注意到这题从蓝题变成紫题,相比没那么简单。
根据题目说给的数据,我们可以发现字典树的最大长度远数组可容纳的范围。于是我就止步于此了(菜鸡就是我,水平就是低)。
如果还要深入学习的大佬可以继续研究一下,大概是优化思路有
拓扑建图优化 树上差分 领接表优化空间存储
当然你也可以或直接改用后缀数组写法(以后可能会讲到)