AC自动机
前置芝士
- kmp
- trie
介绍
学算法首先肯定要清楚这个算法是用来解决啥东西的。
AC 自动机是用线性的复杂度来解决多模匹配的算法。
额(⊙o⊙),说人话就是例如给你一堆字符串(称为模式串)和一个字符串(称为文本串),让你求模式串们在文本串出现的总次数。
来直接看模板题:
题目描述
给定
两个模式串不同当且仅当他们编号不同。
对于
那么好,现在应该很清楚了,这不就是 kmp 的模板题中模式串变多了亿点点吗?如果每个模式串分开匹配,平方级别复杂度,直接爆炸。众所周知:
我们要做的是把所有的模式串建立一个 trie,然后在 trie 上跑 kmp。
可以回想一下 kmp 匹配的过程。有一个数组
它的含义有很多,本质上
如果将
这样的话,在失配的情况下,指针回跳的次数就能最少,可以保证线性的复杂度。
类比一下,trie 上的 kmp 也是一样的
现在已经匹配完了节点
是不是和 kmp 几乎一样?这样,跳到
kmp 其实可以看作 trie 为一条链的 AC 自动机。
举个例子:
she
he
say
shr
her
trie 图为:
绿色的箭头就是
f[2]=4
f[3]=5
f[6]=0
比如在二号节点失配,那么
操作
insert
主要就是建立 trie 的过程,和模板一样的,就不多解释了。
// 这里p为下标,tr为节点编号,tot为节点总数
void insert(string x)
{
int p=0;
for(auto c:x)
{
int u=c-'a';
if(!tr[p][u]) tr[p][u]=++tot;
p=tr[p][u];
}
cnt[p]++;//这里要视情况而定
}
build
先看到 kmp 是如何求的:
int i=1,j=0;
while(isize())
{
//j=f[i-1];
while(j&&s[i]!=s[j]) j=f[j];
if(s[i]==s[j]) j++;
f[++i]=j;
}
当然会有其他的写法,不过也大同小异。注意看注释掉的一行,注释的原因是因为这就是我们上一次循环求得的
还是拿这个例子。
绿色表示已经匹配好了的可以继续用的,红色表示即将要匹配的。
可以看到,要是红色的匹配成功了,自然绿色部分长度加一;若是不成功,就要保证
也就是说我们要用到上一个位置的
至于代码,直接对应 kmp 写就好了,注意第一层的
queue<int> q;
void build()
{
for(int i=0;i<26;i++)
if(tr[0][i])
q.push(tr[0][i]);//根的儿子都为0,直接入队
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<26;i++)//遍历下一层的所有儿子
{
int v=tr[u][i],j=f[u];//j=f[i-1];
while(j&&!tr[j][i]) j=f[j];//while(j&&s[i]!=s[j]) j=f[j];
if(tr[j][i]) j=tr[j][i];//if(s[i]==s[j]) j++;
f[v]=j;//f[++i]=j;
}
}
}
一模一样对吧?等一等,怎么感觉不太对,为啥别人的代码没有 for
里面的 while
?
其实 AC 自动机有一个优化,就是把这个 while
优化掉的。优化后也叫做 trie 图。也就是我们一般所写的形式,统称为 AC 自动机。优化只是优化常数,优化前后其实都是线性的复杂度。
那么好,问题就在于 while
。匹配
这里就可以用类似并查集中路径压缩的方法,将信息存在没有的虚点中,在匹配失败后对号入座就好了。
queue<int> q;
void build()
{
for(int i=0;i<26;i++)
if(tr[0][i])
q.push(tr[0][i]);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<26;i++)
{
int v=tr[u][i];
if(!v) tr[u][i]=tr[f[u]][i];//若失败,继承信息
else f[v]=tr[f[u]][i],q.push(v);//若成功,直接赋值
}
}
}
这就是 AC 自动机建立的终极形态 QWQ。
查询
明白了重点以后,这个就简单很多了,也是由 kmp 的代码直接推过来。
为避免重复,用
只不过当匹配到一个后,它的前缀也要统计答案,而前缀中可能出现的都应该在
int query(string x)
{
int p=0,ans=0;
for(auto c:x)
{
int u=c-'a';
while(p&&!tr[p][u]) p=f[p];//while(j&&t[i]!=s[j]) j=f[j];
if(tr[p][u]) p=tr[p][u];//if(t[i]==s[j]) j++;
int j=p;
while(j&&cnt[j]!=-1)
{
ans+=cnt[j];
cnt[j]=-1;
j=f[j];
}
}
return ans;
}
和 build 同理,这也可以优化,就直接变成了:
int query(string x)
{
int p=0,ans=0;
for(auto c:x)
{
int u=c-'a';
p=tr[p][u];//省去while
int j=p;
while(j&&cnt[j]!=-1)
{
ans+=cnt[j];
cnt[j]=-1;
j=f[j];
}
}
return ans;
}
code
完整代码:
#include
using namespace std;
const int N=1e6+5;
queue<int> q;
int n,tot;
int tr[N][26],cnt[N],f[N];
string s;
void insert(string x)
{
int p=0;
for(auto c:x)
{
int u=c-'a';
if(!tr[p][u]) tr[p][u]=++tot;
p=tr[p][u];
}
cnt[p]++;
}
void build()
{
for(int i=0;i<26;i++)
if(tr[0][i])
q.push(tr[0][i]);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<26;i++)
{
int v=tr[u][i];
if(!v) tr[u][i]=tr[f[u]][i];
else f[v]=tr[f[u]][i],q.push(v);
}
}
}
int query(string x)
{
int p=0,ans=0;
for(auto c:x)
{
int u=c-'a';
p=tr[p][u];
int j=p;
while(j&&cnt[j]!=-1)
{
ans+=cnt[j];
cnt[j]=-1;
j=f[j];
}
}
return ans;
}
int main ()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>s;
insert(s);
}
build();
cin>>s;
cout<<query(s)<<"\n";
return 0;
}
拓扑建图优化
代填的坑 qwq。
一些心得体会
算法学习中,有些可以半懂不懂,比如 kmp,了解
写博客总结的过程中,可以顺便梳理整个过程。带着想要教会别人的目标,不知不觉间自己也能更加深刻地理解。更何况,认真写出一篇博客后,成就感是真真切切的。
前路漫漫,未到抬头之时,我只需,低头前行。
__EOF__