字符串学习笔记

AC自动机

定义:

KMP+Trie=AC自动机

根据 KMP 算法的原理为 Trie 树每个节点建立失配指针进行多模式匹配。

Trie 树:

Trie 中的结点表示的是某个模式串的前缀(状态)。Trie 的边就是状态的转移,将 Trie 树构建后的所有状态的集合记作 Q

失配(fail)指针:

状态 u 的 fail 指针指向另一个状态 v,其中 vQ,且 vu 的最长后缀.

fail 指针与 KMP next 指针的区别:

  • 共同点:两者同样是在失配的时候用于跳转的指针。
  • 不同点:next 指针求的是最长 Border(即最长的相同前后缀),而 fail 指针指向的是所有模式串的前缀中,匹配当前状态的最长后缀。

算法:

构建 fail 指针:

考虑字典树中当前的结点 uu 的父结点是 pp 通过字符 c 的边指向 u,即 trie[p,c]=u。假设深度小于 u 的所有结点的 fail 指针都已求得。

  1. 如果 trie[fail[p],c] 存在:则让 ufail 指针指向 trie[fail[p],c]。相当于在 pfail[p] 后面加一个字符 c,分别对应 ufail[u]

  2. 如果 trie[fail[p],c] 不存在:那么我们继续找到 trie[fail[fail[p]],c]。重复 1 的判断过程,一直跳 fail 指针直到根结点。

  3. 如果真的没有,就让 fail 指针指向根结点。

如此即完成了 fail[u] 的构建。

(如果父亲有对应自己字符指针就随父亲,否则一直找父亲的指针)

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 自动机(加强版)

需要找出哪些模式串在文本串 T 中出现的次数最多

在文本串 S 匹配到第 i 位的时候可以根据 fail 指针找出当前串 S[1,i] 的所有后缀是否在树中出现过,并更新对应的模式串出现次数。

之后排序即可输出最大值。

#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] 玄武密码

对于所有模式串 T,求出其最长的前缀 p,满足 p 是文本串 S 的子串。

先跑一边 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 分解

定义:

定义一个串是 Lyndon 串,当且仅当此串的最小后缀为此串本身。

等价于该串为它所有循环表示中字典序最小的。

Lyndon 分解将任意串 S 划分成字符串序列,满足序列中每个串均为 Lyndon 串且每个串字典序均大于等于其之后的串。

可以证明这种划分存在且唯一,证明略。

算法:

引理 1:若串 uv 都是 Lyndon 串且 u<v,则 u+v 也是 Lyndon 串。

证明略。

引理2:若字符串 u 和字符 c 满足 u+c 是某个 Lyndon 串的前缀,则对于字符 d>cu+dLyndon 串。

证明:

设该 Lyndon 串为 u+c+t

则根据性质就有 i[2,len(u)],u[i:]+c+t>u+c+t

也就是说 u[i:]+cu,(即为 u 的后缀加上字符 c 后字典序仍比 u 大)。

所以就有 u[i:]+d>u[i:]+cu

同时因为 c>u[1],就有 d>c>u[1]

u+dLyndon 串。

Duval 算法:

这个算法可以在 O(n) 时间复杂度,O(1) 空间复杂度内求出一个字符串 SLyndon 分解。

维护三个变量 i,j,ki,k 将字符串分为三段。

S[:i1] 为已经分解好且满足字典序非递增的 gLyndon 串。(s1s2s3sg)

S[i,k1]=th+v(h>1) 尚未分解,满足 tLyndon 串,且 vt 的一个可为空且不等于 t 的前缀,且有 sg>S[i,k1]

程序实现时按顺序读入字符 S[k],令 j=k|t|

S[j]S[k] 为依据分类讨论。

  • S[k]=S[j] 时,直接 kk+1,jj+1,尾部字符串 v 的周期 kj 继续保持
  • S[k]>S[j] 时,由 引理 2 可知 v+S[k]Lyndon 串,由于 Lyndon 分解需要满足 sisi+1,所以继续向前合并,并且最终整个 th+v+S[k] 会形成一个新的 Lyndon 串(所以将 j 调回 i 的位置继续判断)。
  • S[k]<S[j] 时,th 的分解被固定下来,算法从 v 的开头处重新开始,之前的都归到 i 前的第一部分。

i 只会单调往右移动,同时 k 每次移动的距离不会超过 i 移动的距离,所以时间复杂度是 O(n) 的。

核心代码:

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;
}
}

一个子串的左端点为 i,右端点为 i+kj1

例题:

P6114 【模板】Lyndon 分解

本题只需要输出所有右端点的异或和,在 while 循环中直接统计即可。

P1368 【模板】最小表示法

对于长度为 n 的字符串 S,设 T=S+S,对 T 进行 Lyndon 分解,找到首字符位置 n 且最大的 Lyndon 串,这个串的首字符即最小表示法的首字符。

#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;
}
posted @   RoFtaCD  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示

目录导航