AC自动机学习笔记
AC 自动机
此博客参考 Luckyblock 的博客,以及此博客还有以及pokefunc的讲解
由于我太弱了所以我花了近一天的时间来理解这个 由于我一开始看了好几篇导致我现在脑子里还是混乱的。
我已经傻了所以我没讲明白以后再来
前置知识#
AC 自动机可以处理一个字符串在一篇文章也就是多个字符串中的出现情况。
算法流程#
AC 自动机需要提前知道所有需要进行匹配的字符串,例如:say,she,shr,her。接下来我们需要把他们建成一棵字典树。
就像上面这张图一样,构造出了一棵字典树,然后我们就可以进行下一步——求失配指针。
这一步也是最重要的一步,我们都知道 KMP 算法里面是有一个
我们在匹配的过程中一旦匹配到当前位置没有成功匹配的话,就会直接跳转到当前点的失配指针指向的点继续进行匹配。
先来瞅一下代码。
inline void get_fail()
{
int l=0,r=0;
for(int i=0;i<26;i++)
{
if(e[0].ch[i]!=0)
{
e[e[0].ch[i]].fail=0;
q[++r]=e[0].ch[i];
}
}
while(l<r)
{
int u=q[++l];
for(int i=0;i<26;i++)
{
int v=e[u].ch[i];
if(v)
{
e[v].fail=e[e[u].fail].ch[i];
q[++r]=v;
}
else e[u].ch[i]=e[e[u].fail].ch[i];
}
}
}
对于第二层的节点,最大的特点就是要特殊处理,因为第二层的点的在字典树上出现的最大后缀肯定是当前点的单个字符,也就是说
然后这时我们需要一个队列来存放已经有
也不知道我讲明白了没有
在查询的过程中我们需要把大串一个一个的去匹配,这样才能让每一个点都正确的匹配上,如果当前点有
P3808 【模板】AC 自动机(简单版)#
#include<bits/stdc++.h>
#define int long long
#define N 1010000
using namespace std;
int n,cnt,q[N];
struct sb{int fail,num,ch[30];}e[N];
string s;
inline void build()
{
int len=s.size(),u=0;
for(int i=0;i<len;i++)
{
int c=s[i]-'a';
if(e[u].ch[c]==0)e[u].ch[c]=++cnt;
u=e[u].ch[c];
}
e[u].num++;//标记最后一个字符的位置
}
inline void get_fail()
{
int l=0,r=0;
for(int i=0;i<26;i++)
{
if(e[0].ch[i]!=0)
{
e[e[0].ch[i]].fail=0;
q[++r]=e[0].ch[i];
}
}
while(l<r)
{
int u=q[++l];
for(int i=0;i<26;i++)
{
int v=e[u].ch[i];
if(v)
{
e[v].fail=e[e[u].fail].ch[i];
q[++r]=v;
}
else e[u].ch[i]=e[e[u].fail].ch[i];
}
}
}
inline int ACzdj()
{
int len=s.size();
int u=0,ans=0;
for(int i=0;i<len;i++)
{
int c=s[i]-'a';
u=e[u].ch[c];
int v=u;
while(v&&e[v].num!=-1)
{
ans+=e[v].num;
e[v].num=-1;
v=e[v].fail;
}
}
return ans;
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>s;
build();
}
e[0].fail=0;
get_fail();
cin>>s;
cout<<ACzdj()<<endl;
return 0;
}
P3796 【模板】AC 自动机(加强版)#
因为题目中说过了保证不出现重复的字符串,所以我们要想在最后查询是哪个或者哪些字符串的话,我们可以在建字典树的时候将原来表示个数的
#include<bits/stdc++.h>
#define int long long
#define N 300100
using namespace std;
int n,cnt,maxn,num[N],ch[N][30],fail[N],ans[N];
string s1[N],s2;
inline void qk()
{
memset(num,0,sizeof(num));
memset(ans,0,sizeof(ans));
memset(ch,0,sizeof(ch));
memset(fail,0,sizeof(fail));
cnt=0;maxn=0;
}
inline void insert(string s,int k)
{
int u=0;
for(int i=0;i<s.size();i++)
{
int c=s[i]-'a';
if(ch[u][c]==0)ch[u][c]=++cnt;
u=ch[u][c];
}
num[u]=k;
}
inline void get_fail()
{
int now=0;
queue<int>q;
for(int i=0;i<26;i++)
if(ch[0][i])
q.push(ch[0][i]);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
int v=ch[u][i];
if(v)
{
fail[v]=ch[fail[u]][i];
q.push(v);
}
else ch[u][i]=ch[fail[u]][i];
}
}
}
inline void ACzdj(string s)
{
int u=0;
for(int i=0;i<s.size();i++)
{
u=ch[u][s[i]-'a'];
for(int j=u;j;j=fail[j])
ans[num[j]]++;
}
}
signed main()
{
while(cin>>n)
{
if(n==0)break;
qk();
for(int i=1;i<=n;i++)
cin>>s1[i],insert(s1[i],i);
get_fail();
cin>>s2;
ACzdj(s2);
for(int i=1;i<=n;i++)
// cout<<ans[i]<<' ',
maxn=max(maxn,ans[i]);
// cout<<endl;
cout<<maxn<<endl;
for(int i=1;i<=n;i++)
if(ans[i]==maxn)
cout<<s1[i]<<endl;
}
return 0;
}
失配树(拓展)#
据学长所说失配树就是在 AC 自动机上令
来看一下学长博客里的图:
上图里的
长度为
可以看出,
这棵树也就是所谓的失配树,通过对于一个字符串建出这棵树,我们可以快速找出其多组长度不同的前缀的最长公共
解释一下原理:
如果
也就是说处理出
所以说,如果两个前缀能通过跳
这个过程和我们树上找 LCA 的方式很像,所以我们可以将其建成一棵树。
我承认就是从学长博客搬过来的
P5829 【模板】失配树#
#include<bits/stdc++.h>
#define int long long
#define N 1000010
using namespace std;
string s;
int n,q,fail[N],dis[N],fa[N][25],a,b;
inline void init()
{
int p=0;
for(int i=2;i<=n;i++)
{
while(p&&s[p+1]!=s[i])
p=fail[p];
if(s[p+1]==s[i])
p++;
fail[i]=p;
dis[i]=dis[p]+1;
fa[i][0]=p;
}
for(int k=1;k<=21;k++)
{
for(int i=1;i<=n;i++)
fa[i][k]=fa[fa[i][k-1]][k-1];
}
}
inline int up(int x,int h)
{
if(dis[x]<=h)return x;
for(int i=21;i>=0;i--)
{
if(dis[fa[x][i]]>h)
x=fa[x][i];
}
return fa[x][0];
}
inline int LCA(int x,int y)
{
if(dis[x]<dis[y])
swap(x,y);
x=up(x,dis[y]);
for(int i=21;i>=0;i--)
{
if(fa[x][i]!=fa[y][i])
{
x=fa[x][i];
y=fa[y][i];
}
}
return fa[x][0];
}
signed main()
{
cin>>s>>q;
n=s.size();
s.insert(s.begin(),' ');
init();
while(q--)
{
cin>>a>>b;
cout<<LCA(a,b)<<endl;
}
return 0;
}
P5357 【模板】AC 自动机(二次加强版)#
#include<bits/stdc++.h>
#define N 201000
using namespace std;
int n, num[N], ch[N][30], fail[N], ans[N], cnt;
//num存放当前编号的字符串在字典树中结尾节点的编号,ans存放当前串出现的次数
string s1[N], s2;//s2是主串,s1是模式串
vector<int> e[N];//存fail树的边
inline void insert(string s, int x)
{
int u = 0, len = s.size();
for(int i = 0; i < len; i ++)
{
int c = s[i] - 'a';
if(!ch[u][c]) ch[u][c] = ++ cnt;
u = ch[u][c];
}
num[x] = u;//和上面说的对应,记录当前编号的字符串在字典树中的节点
return ;
}
inline void get_fail()//正常的求fail过程
{
queue<int> q;
for(int i = 0; i < 26; i ++)
if(ch[0][i]) q.push(ch[0][i]);
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = 0; i < 26; i ++)
{
int v = ch[u][i];
if(v) fail[v] = ch[fail[u]][i], q.push(v);
else ch[u][i] = ch[fail[u]][i];
}
}
return ;
}
inline void AC()//在这里不乱跳fail指针了,由后面在fail树上的dfs一起处理
{
int len = s2.size(), u = 0;
for(int i = 0; i < len; i ++)
{
int v = s2[i] - 'a';
u = ch[u][v];
ans[u] ++;//不标记而是直接只存下当前节点的值
}
return ;
}
inline void dfs(int u)
{
for(auto v : e[u])
{
dfs(v);//先搜索,在回溯的过程中从后往前给他搜出来
ans[u] += ans[v];//因为fail树上面节点出现的次数一定是字数节点出现次数的和
}
return ;
}
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> s1[i];
insert(s1[i], i);
}
get_fail();//求fail指针
for(int i = 1; i <= cnt; i ++)//枚举每一个节点
e[fail[i]].push_back(i);//建一条由fail_{i}到i的有向边,建立fail树
cin >> s2;
AC();
dfs(0);
for(int i = 1; i <= n; i ++)
cout << ans[num[i]] << endl;
return 0;
}
作者: 北烛青澜
出处:https://www.cnblogs.com/Multitree/p/17073267.html
本站使用「CC BY 4.0」创作共享协议,转载请在文章明显位置注明作者及出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!