AcWing 算法提高课 AC自动机
AC自动机=Trie+kmp
优化:Trie图
1、kmp
长字符串s和模板串p都以下标1开始。
(1) 求next数组:kmp的next数组存的是p的自匹配,即以p[i]为结尾的后缀能够匹配的最长非平凡(不是自身)前缀。由于非平凡,next[0]=next[1]=0,循环从2开始。
(2)进行匹配,将模板串p在长字符串s上移动,并求出当前可以匹配的最长长度,如果此长度为p的长度,则匹配成功。循环从1开始(可能p长度为1并且直接匹配成功 )
2、AC自动机模板:
AC自动机可以解决和多个模板串匹配的问题,用BFS在trie上建立ne数组,匹配过程和kmp类似。
例题:https://www.acwing.com/blog/content/404/
模板:统计模板串在长字符串中出现的个数

int n; const int N=10010; const int S=55,M=1000010; int tr[N*S][26];//自动机数组,第一维节点,第二维子节点 int cnt[N*S];//以当前节点结尾的单词数量 int idx; char str[M]; int que[N*S]; int ne[N*S]; void Insert() { int p=0; for(int i=0;str[i];i++) { int t=str[i]-'a'; if(!tr[p][t]) tr[p][t]=++idx; p=tr[p][t]; } cnt[p]++; } void Build() { int hh=0,tt=-1; for(int i=0;i<26;i++) { if(tr[0][i]) que[++tt]=tr[0][i]; } while(hh<=tt) { int t=que[hh++]; for(int i=0;i<26;i++) { int c=tr[t][i]; if(!c) continue; int j=ne[t]; while(j&&!tr[j][i]) j=ne[j]; if(tr[j][i]) j=tr[j][i]; ne[c]=j; que[++tt]=c; } } } void YD() { memset(tr,0,sizeof(tr)); memset(cnt,0,sizeof(cnt)); memset(ne,0,sizeof(ne)); idx=0; cin>>n; while(n--) { cin>>(str); Insert();//trie插入 } Build();//构建ac自动机 cin>>str; int res=0; for(int i=0,j=0;str[i];i++) { int t=str[i]-'a'; while(j&&!tr[j][t]) j=ne[j]; if(tr[j][t]) j=tr[j][t]; //统计答案不光要统计j 还要统计更短的可能出现的单词,即往ne[j]去查询 int p=j; while(p) { res+=cnt[p]; cnt[p]=0; p=ne[p]; } } cout<<res<<endl; }
3、Trie图
相当于把AC自动机路径压缩,注意和2中模板里,build函数和统计过程中的不同。
代码更短,常数更小。
例题:https://www.acwing.com/blog/content/404/
模板:统计模板串在长字符串中出现的个数

#include<bits/stdc++.h> #define fore(x,y,z) for(LL x=(y);x<=(z);x++) #define forn(x,y,z) for(LL x=(y);x<(z);x++) #define rofe(x,y,z) for(LL x=(y);x>=(z);x--) #define rofn(x,y,z) for(LL x=(y);x>(z);x--) #define pub push_back #define all(x) (x).begin(),(x).end() #define fi first #define se second using namespace std; typedef long long LL; typedef pair<int,int> PII; typedef pair<LL,LL> PLL; int n; const int N=10010; const int S=55,M=1000010; int tr[N*S][26];//自动机数组,第一维节点,第二维子节点 int cnt[N*S];//以当前节点结尾的单词数量 int idx; char str[M]; int que[N*S]; int ne[N*S]; void Insert() { int p=0; for(int i=0;str[i];i++) { int t=str[i]-'a'; if(!tr[p][t]) tr[p][t]=++idx; p=tr[p][t]; } cnt[p]++; } void Build() { int hh=0,tt=-1; for(int i=0;i<26;i++) { if(tr[0][i]) que[++tt]=tr[0][i]; } while(hh<=tt) { int t=que[hh++]; for(int i=0;i<26;i++) { int c=tr[t][i]; if(!c) tr[t][i]=tr[ne[t]][i]; //如果没有这个儿子,则直接把tr指向上一个匹配位置 else { ne[c]=tr[ne[t]][i]; //如果上一个有则直接匹配,如果没有,则已经被压缩 que[++tt]=c; } } } } void YD() { memset(tr,0,sizeof(tr)); memset(cnt,0,sizeof(cnt)); memset(ne,0,sizeof(ne)); idx=0; cin>>n; while(n--) { cin>>(str); Insert();//trie插入 } Build();//构建ac自动机 cin>>str; int res=0; for(int i=0,j=0;str[i];i++) { int t=str[i]-'a'; j=tr[j][t]; int p=j; while(p) { res+=cnt[p]; cnt[p]=0; p=ne[p]; } } cout<<res<<endl; } int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); int T=1; cin >> T; while (T--) { YD(); } return 0; }
4、例题
AC自动机上dp:dp[i][j]代表移动i位且匹配到trie的j位置时的最小修改次数
注意bad标识了坏前缀
https://www.acwing.com/problem/content/1055/
Trie图+dp+拓扑排序:统计文章中全部单词各自的出现次数(作为其他单词的子串也算)
https://www.acwing.com/problem/content/1287/
此题的思想是,将单词出现,转化为单词作为全部前缀的后缀出现,对应ne的定义(前缀的后缀)。
cnt统计的就是单词出现次数,插入时统计的是作为平凡(即后缀就是本身)后缀的前缀出现的次数,拓扑排序累加的过程是累加作为非平凡后缀的前缀出现的次数,累加后就是作为全部后缀的前缀出现的次数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人