前言——不明觉厉的算法排名第一位
关于AC自动机
必要的前置知识
KMP
字典树
什么是AC自动机
AC自动机以字典树结构为基础,结合KMP的思想建立
简单来讲,建立一个AC自动机有两个步骤:
第一步: 建立字典树,将所有的模式串插入,构成一棵字典树
第二步:依据KMP的思想,对字典树上的所有结点构造失配指针
AC自动机解决什么问题?
有多个模式串的匹配问题,即多模匹配问题(在一个字符串中寻找多个模式串)
构建一个AC自动机
字典树的构建
点击这里快速回忆
失配指针的构造
AC自动机使用fail指针来帮助多模式串进行匹配
树上结点u的fail指针指向另一个树上结点v,u和 v两者的关系是:v是u的最长后缀
对比AC自动机的fail指针和KMP的next指针:
相同点:是在失配时用于跳转的指针
不同点:next指针得到的是单个模式串最长的相同前后缀,fail指针指向所有模式串的前缀中匹配当前状态的最长后缀
构造fail指针的基础思想
考虑字典树中的当前结点u,u的父节点是p,p通过字符串c的边指向u,即有trie[p,c]=u。假设深度小于u的所有节点的fail指针都已求得。
第一种情况:
如果trie[fail[p],c]存在,让u的fail指针指向 trie[fail[p],c],fail[p]是p的后缀,p和fail[p]在字符串末尾一起加上一个字母c不会破坏fail[p]作为后缀的地位(前提是能够添加c)
第二种情况:
如果trie[fail[p],c]不存在(fail[p]后不存在c可供添加),那么,继续寻找trie[fail[fail[p]],c]。不断重复判断过程,直到fail指针跳到根节点
第三种情况:
到达根节点,fail指针指向根节点
转一张OI-WIKI上的图像方便理解一波
构建失配指针与自动机
使用BFS遍历字典树来进行构造
所谓的 fail 指针其实就是当前结点u对应串S的一个后缀集合。
void getFail()//建立fail指针
{
for(int i=0;i<26;i++)//0点全部指向根节点
{
trie[0].son[i]=1;
}
q.push(1);//放入根节点,开始广搜建立
while(!q.empty())
{
int u = q.front();
q.pop();
int Fail = trie[u].fail;
for(int i=0;i<26;i++)
{
int v=trie[u].son[i];
if(!v)//v点尚未被建立,也就是字典树中没有该节点被插入
{
trie[u].son[i] = trie[Fail].son[i];//尝试指向u的fail指针的对应结点
continue;
}
trie[v].fail = trie[Fail].son[i];//存在,直接标记fail值
q.push(v);
}
}
}
多模式匹配函数query()
void query(string s)//在树上进行模式串匹配
{
int u=1,len=s.size();
for(int i=0;i<len;i++)
{
u=trie[u].son[s[i]-'a'];//因为有fail指针,可以放心遍历
trie[u].ans++;//记录了某个前缀的出现次数
}
}
板子
超级优化模板
使用了拓扑排序,通常情况是不需要的
some tips:
暴力跳fail的最坏时间复杂度为O(T.size()*sum_of_s.size())
经过拓扑建图优化的AC自动机的时间复杂度O(T.size())
将fail想象成有向边,如果在一个点进行操作,沿着该点的连出去的点也会进行操作
#include<bits/stdc++.h>
#define fast ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
using namespace std;
const int N = 2e6+5;
const int char_set = 26;
string s,T;
int n,cnt,vis[200051],ans,in[N],Map[N];
//为什么不用map? => 时间复杂度O(1)和O(logn)的区别
struct tr//字典树结点的结构体
{
int son[char_set], fail, flag, ans;
void clear()//初始化函数
{
memset(son,0,sizeof(son)), fail = flag = ans = 0;
}
}trie[N];
queue<int>q;
void insert(string s,int num)//字典树插入
{
int u=1, len=s.size();//根节点与字符串长度
for(int i=0;i<len;i++)//遍历字符串
{
int v=s[i]-'a';//
if(!trie[u].son[v])//字典树该节点尚不存在
{
trie[u].son[v]=++cnt;//标记该节点
}
u=trie[u].son[v];//向下遍历字典树
}
if(!trie[u].flag)//标记字典树该节点对应的字符串
{
trie[u].flag=num;
}
Map[num]=trie[u].flag;//map存字符串的尾结点位置
}
void getFail()//建立fail指针
{
for(int i=0;i<26;i++)//0点全部指向根节点
{
trie[0].son[i]=1;
}
q.push(1);//放入根节点,开始广搜建立
while(!q.empty())
{
int u = q.front();
q.pop();
int Fail = trie[u].fail;
for(int i=0;i<26;i++)
{
int v=trie[u].son[i];
if(!v)//v点尚未被建立,也就是字典树中没有该节点被插入
{
trie[u].son[i] = trie[Fail].son[i];//尝试指向u的fail指针的对应结点
continue;
}
trie[v].fail = trie[Fail].son[i];//存在,直接标记fail值
in[trie[v].fail]++;//同时v点入度加一
q.push(v);
}
}
}
//通常情况不需要执行下面这个函数
void topu()//拓扑排序,避免重复遍历,入度为0的点遍历一次后不会被重复遍历
{
for(int i=1;i<=cnt;i++)//最初,标记哪些点能够直接出去
{
if(in[i]==0)//入度为0,可以去掉
{
q.push(i);
}
}
while(!q.empty())//入度为0的点不为0时
{
int u=q.front();//取点
q.pop();
vis[trie[u].flag] = trie[u].ans;//统计串的出现次数
int v = trie[u].fail;//向上跳fail,找后缀
in[v]--;
trie[v].ans += trie[u].ans;//后缀加上完整串的出现次数
if(in[v]==0)//入度为0即可准备去点
{
q.push(v);
}
}
}
void query(string s)//在树上进行模式串匹配
{
int u=1,len=s.size();
for(int i=0;i<len;i++)
{
u=trie[u].son[s[i]-'a'];//因为有fail指针,可以放心遍历
trie[u].ans++;//记录了某个前缀的出现次数
}
}
int main()
{
fast;
cin >> n;
cnt=1;
for(int i=1;i<=n;i++)
{
cin >> s;
insert(s,i);//插入字符串
}
getFail();//得fail指针
cin >> T;
query(T);//查看模式串
topu();//进行拓扑排序,并统计答案
for(int i=1;i<=n;i++)
{
cout << vis[Map[i]] << endl;
}
}
习题集:
P3041 [USACO12JAN]Video Game G
AC自动机与DP相结合的问题:
首先:构建一颗带fail指针的字典树
完成之后,开始进行DP,通常,状态转移方程的第一维是字符的个数,第二维是字典树上结点总数,根据条件可以增加维数。
具体解法请见下方:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
#define ll long long
#define fast ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
const int char_set = 3;
const int N = 3e2+5;
const int K = 1e3+5;
int cnt = 1;
int n, k;
int dp[K][N];//3e5
struct st
{
int son[char_set], fail, num;
void clear()
{
for(int i=0;i<char_set;++i)
{
son[i] = 0;
fail = 0;
num = 0;
}
}
}trie[N];
void insert(string s)//插入,懂得都懂
{
int u=1, len = s.size();
for(int i=0;i<len;++i)
{
int c = s[i]-'A';
int v = trie[u].son[c];
if(!v)
{
trie[u].son[c] = ++cnt;
}
u = trie[u].son[c];
}
trie[u].num++;
}
queue<int> q;
void getFail()//构建fail指针,懂得都懂
{
for(int i=0;i<char_set;++i)
{
trie[0].son[i] = 1;
}
q.push(1);
while(q.size()>0)
{
int u = q.front();
q.pop();
int fail = trie[u].fail;
for(int i=0;i<char_set;++i)
{
int v = trie[u].son[i];
if(!v)
{
trie[u].son[i] = trie[fail].son[i];
continue;
}
trie[v].fail = trie[fail].son[i];
q.push(v);
}
trie[u].num += trie[trie[u].fail].num;//并统计作为后缀满足的个数
}
}
int solve()//dp部分,我滴软肋
{
for(int i=0;i<=k;++i)
{
for(int j=2;j<=cnt;++j)
{
dp[i][j] = -1e9;
}
}
for(int t=1;t<=k;++t)
{
for(int i=1;i<=cnt;++i)
{
for(int j=0;j<char_set;++j)
{
dp[t][trie[i].son[j]] = max(dp[t][trie[i].son[j]],dp[t-1][i]+trie[trie[i].son[j]].num);
}
}
}
int ans = 0;
for(int i=0;i<=cnt;++i)
{
ans = max(ans,dp[k][i]);
}
return ans;
}
int main()
{
fast;
cin >> n >> k;
for(int i=1;i<=n;++i)
{
string s;
cin >> s;
insert(s);
}
getFail();
cout << solve() << endl;
return 0;
}