字符串学习笔记
AC自动机
定义:
根据 KMP 算法的原理为 Trie 树每个节点建立失配指针进行多模式匹配。
Trie 树:
Trie 中的结点表示的是某个模式串的前缀(状态)。Trie 的边就是状态的转移,将 Trie 树构建后的所有状态的集合记作
失配(fail)指针:
状态
fail 指针与 KMP next 指针的区别:
- 共同点:两者同样是在失配的时候用于跳转的指针。
- 不同点:next 指针求的是最长 Border(即最长的相同前后缀),而 fail 指针指向的是所有模式串的前缀中,匹配当前状态的最长后缀。
算法:
构建 fail 指针:
考虑字典树中当前的结点
-
如果
存在:则让 的 指针指向 。相当于在 和 后面加一个字符 ,分别对应 和 。 -
如果
不存在:那么我们继续找到 。重复 1 的判断过程,一直跳 fail 指针直到根结点。 -
如果真的没有,就让 fail 指针指向根结点。
如此即完成了
(如果父亲有对应自己字符指针就随父亲,否则一直找父亲的指针)
void build() { for (int i = 0; i < 26; i++) if (tr[0][i]) q.push(tr[0][i]); while (q.size()) { int u = q.front(); q.pop(); for (int i = 0; i < 26; i++) { if (tr[u][i]) fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]); else tr[u][i] = tr[fail[u]][i]; } } }
在构建自动机时,每次匹配都会一直向 fail 指针跳边来找到所有的匹配,但是这样的效率较低,需要优化建图。
可以按照 fail 树建图。
queue<int> q; void bfs(){ //nxt == fail for(int i=0;i<26;++i) ch[0][i]=1; q.push(1),nxt[1]=0; while(!q.empty()){ int x=q.front(),v;q.pop(); for(int i=0;i<26;++i){ if(!ch[x][i]) ch[x][i] = ch[nxt[x]][i]; else{ q.push(ch[x][i]),v=nxt[x]; while(v && !ch[x][i]) v=nxt[v]; nxt[ch[x][i]]=ch[v][i]; } } } }
例题:
P3796 【模板】AC 自动机(加强版)
需要找出哪些模式串在文本串
在文本串
之后排序即可输出最大值。
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+105,inf=0x3f3f3f3f; struct Tree {int fail,end,vis[26];}AC[N]; int cnt; string s[N]; struct Res{int num,pos;}Ans[N]; bool operator <(const Res &a,const Res &b){ if(a.num!=b.num) return a.num>b.num; else return a.pos<b.pos; } void Clean(int x){memset(AC[x].vis,0,sizeof AC[x].vis),AC[x].fail=AC[x].end=0;} inline void Build(string s,int Num){ int l=s.size(); int now=0; for(int i=0,c;i<l;++i){ c=s[i]-'a'; if(AC[now].vis[c]==0) AC[now].vis[c]=++cnt,Clean(cnt); now=AC[now].vis[c]; } AC[now].end=Num; } void Get_fail(){ queue<int> Q; for(int i=0;i<26;++i) if(AC[0].vis[i]!=0) AC[AC[0].vis[i]].fail=0,Q.push(AC[0].vis[i]); while(!Q.empty()){ int u=Q.front(),fail=AC[u].fail; Q.pop(); for(int i=0,v;i<26;++i){ v=AC[u].vis[i]; if(v!=0) AC[v].fail=AC[fail].vis[i],Q.push(v); else AC[u].vis[i]=AC[AC[u].fail].vis[i]; } } } int AC_Query(string s){ int l=s.size(); int now=0,ans=0; for(int i=0;i<l;++i){ now=AC[now].vis[s[i]-'a']; for(int t=now;t;t=AC[t].fail) ++Ans[AC[t].end].num; } return ans; } int main(){ int n; while(1){ cin>>n;if(n==0) break; cnt=0,Clean(0); for(int i=1;i<=n;++i) cin>>s[i],Ans[i]={0,i},Build(s[i],i); AC[0].fail=0,Get_fail(); cin>>s[0],AC_Query(s[0]); sort(Ans+1,Ans+1+n); cout<<Ans[1].num<<'\n'; cout<<s[Ans[1].pos]<<'\n'; for(int i=2;i<=n;++i){ if(Ans[i].num == Ans[i-1].num) cout<<s[Ans[i].pos]<<'\n'; else break; } } return 0; }
是为 AC 自动机模板题。
P5231 [JSOI2012] 玄武密码
对于所有模式串
先跑一边 AC 自动机,处理出 fail 数组,然后再把文本串匹配一下,可以获得 vis 数组,代表着 trie 树上的这个点是文本串的前缀。最后只需要再每个模式串跑一遍 trie 树,就可以得到最长的公共前缀长度了。
#include<bits/stdc++.h> using namespace std; const int N=1e7+105,M=1e5+105; inline int mk(char c){if(c=='E') return 1;if(c=='W') return 2;if(c=='S') return 3;return 4;} int n,m; int tr[N][5],tt,fail[N],ep[N]; bool vis[N]; char T[M][105],S[N]; void ins(char *s,int len){ int u=0; for(int i=0,c=s[0];i<len;++i,c=s[i]) u = tr[u][c] ? tr[u][c] : tr[u][c]=++tt; ++ep[u]; } queue<int> q; void build(){ for(int i=0;i<5;++i) if(tr[0][i]) q.push(tr[0][i]),fail[tr[0][i]]=0; while(!q.empty()){ int u=q.front();q.pop(); for(int i=0;i<5;++i){ if(tr[u][i]) fail[tr[u][i]]=tr[fail[u]][i],q.push(tr[u][i]); else tr[u][i]=tr[fail[u]][i]; } } int p=0; for(int i=0;i<n;++i){ p=tr[p][S[i]]; for(int k=p;k&&!vis[k];k=fail[k]) vis[k]=1; } } int query(char *s){ int len=strlen(s),p=0,res=0; for(int i=0;i<len;++i){ p=tr[p][s[i]]; if(vis[p]) res=i+1; } return res; } int main(){ scanf("%d%d",&n,&m); scanf("%s",S); for(int i=0;i<n;++i) S[i]=mk(S[i]); for(int i=1,l;i<=m;++i){ scanf("%s",T[i]),l=strlen(T[i]); for(int j=0;j<l;++j) T[i][j]=mk(T[i][j]); ins(T[i],l); } build(); for(int i=1;i<=m;++i) printf("%d\n",query(T[i])); return 0; }
PAM(回文自动机)
待补充
.
.
.
SAM(后缀自动机)
待补充
.
.
.
Lyndon 分解
定义:
定义一个串是
等价于该串为它所有循环表示中字典序最小的。
可以证明这种划分存在且唯一,证明略。
算法:
引理 1:若串
证明略。
引理2:若字符串
证明:
设该
则根据性质就有
也就是说
所以就有
同时因为
故
算法:
这个算法可以在
维护三个变量
程序实现时按顺序读入字符
以
- 当
时,直接 ,尾部字符串 的周期 继续保持 - 当
时,由 引理 2 可知 是 串,由于 分解需要满足 ,所以继续向前合并,并且最终整个 会形成一个新的 串(所以将 调回 的位置继续判断)。 - 当
时, 的分解被固定下来,算法从 的开头处重新开始,之前的都归到 前的第一部分。
核心代码:
for(int i=1;i<=n;){ int j=i,k=i+1; while(k<=n && s[k]>=s[j]){ //前两种情况 if(s[k]>s[j]) j=i; else ++j; ++k; } while(i<=j){ // 在此处获取字串信息 // 每个字串的长度均为 k-j i+=k-j; } }
一个子串的左端点为
例题:
P6114 【模板】Lyndon 分解
本题只需要输出所有右端点的异或和,在 while 循环中直接统计即可。
P1368 【模板】最小表示法
对于长度为
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e6+105,inf=0x3f3f3f3f; int n,ans,s[N]; int main() { scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",s+i),s[i+n]=s[i]; int i=1; while(i<=n){ int j=i,k=i+1; while(k<=n*2 && s[j]<=s[k]) j = (s[j]==s[k++]) ? j+1 : i; while(i<=j) i+=k-j,ans = i<=n ? i : ans; } for(int i=1;i<=n;++i) printf("%d ",s[ans-1+i]); printf("\n"); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】