1014 words
5 minutes
String
2025-11-28

字符串#

子串可以看成前缀的一个后缀

前缀函数#

π[i]\pi[i]s[0...i]s[0...i]最长的相等的真前缀真后缀的长度。 π[i]=maxk=0...ik,s[0...k1]=s[ik+1...i]\pi[i]=\max_{k=0...i}k,s[0...k-1]=s[i-k+1...i] 特别的规定π[0]=0\pi[0]=0 假设我们已经知道前iiπ\pi函数值,考虑计算π[i+1]\pi[i+1],如果s[i+1]=s[π[i]+1]s[i+1]=s[\pi[i]+1],那么,π[i+1]=π[i]+1\pi[i+1]=\pi[i]+1,如果不相等,我们考虑找到一个第二长的相等的真前缀与真后缀的长度,发现s[0...π[i]]s[iπ[i]+1...i]s[0...\pi[i]]和s[i-\pi[i]+1...i]完全相等,那么我们要找的第二长是不是就是s[0...π[i]]s[0...\pi[i]]最长,所以,我们这样一步一步跳即可。

void pre(string s){
int len=s.length();
pi[0]=0;
for (int i=1;i<len;i++){
int j=pi[i-1];
while(j&&s[i]!=s[j])j=pi[j-1];
pi[i]=j+(s[i]==s[j]);
}
}

KMP#

给定一个文本 𝑡和一个字符串 𝑠,我们尝试找到并展示 𝑠 在 𝑡 中的所有出现 我们发现,只要处理s+s+ # +t+t就可以 #代指不在s,ts,t的任意一个字符 代码同上

AC自动机#

Trie 上的自动机 建立需要两个步骤 1.基础的 Trie 结构:将所有的模式串构成一棵 Trie; 2.KMP 的思想:对 Trie 树上所有的结点构造失配指针。 字典树正常建树就可以,我们来看一下失配指针 fail指针指向所有模式串的前缀中匹配当前状态的最长后缀,上文的π\pi指向模式串,接下来我们考虑怎么计算fail函数 仔细想一下,发现其实和π\pi函数求法是一样的。 考虑字典树中当前的结点 𝑢,𝑢的父结点是 𝑝,𝑝通过字符 𝑐的边指向 𝑢,即 trie(𝑝,𝑐)=𝑢trie⁡(𝑝,𝑐) =𝑢。假设深度小于 𝑢 的所有结点的failfail指针都已求得。 1.如果 trie(fail(𝑝),𝑐)trie⁡(fail⁡(𝑝),𝑐) 存在:则让 𝑢的 fail 指针指向 trie(fail(𝑝),𝑐)trie⁡(fail⁡(𝑝),𝑐)。相当于在 𝑝𝑝fail(𝑝)fail⁡(𝑝)后面加一个字符 𝑐,分别对应 𝑢 和 fail(𝑢)fail⁡(𝑢); 2.如果 trie(fail(𝑝),𝑐)trie⁡(fail⁡(𝑝),𝑐) 不存在:那么我们继续找到 trie(fail(fail(𝑝)),𝑐)trie⁡(fail⁡(fail⁡(𝑝)),𝑐)。重复判断过程,一直跳failfail指针直到根结点; 3.如果依然不存在,就让 failfail 指针指向根结点。

建树过程#

比较抽象,什么时候我想到一个说法我再详细说 我们考虑优化一下第二步的跳跃,发现这是在跳failfail指针,如果我们将failfail指针的结构加到树上会怎么样呢?

void build(){
//建立ac自动机
while (!Q.empty()) Q.pop();
for (int i=0;i<26;i++)
if (T[0].son[i])Q.push(T[0].son[i]);
while (!Q.empty()){
int u=Q.front();Q.pop();
for (int i=0;i<26;i++){
if (T[u].son[i]){
T[T[u].son[i]].fail=T[T[u].fail].son[i];
Q.push(T[u].son[i]);
}
else T[u].son[i]=T[T[u].fail].son[i];//改变树的结构
}
}
}

简单画个图,我们发现改变树的结构帮我们一次跳到了所需位置(后期再补)

查询操作及其优化#

首先,我们先考虑ACAM上每个节点是什么意思。每个节点可以看做是一个字符串的前缀(从根节点到当前节点所经过的边形成的前缀)。我们称其为该点的状态。 那我们的failfail指针又是干什么的的呢?我们发现,failfail指针指向当前穿的最长的后缀。 那么,我们每次查寻到一个点,首先,能达到该点的状态,然后其failfail所指的状态也能达到,每次到一个点,我们就跳failfail来统计答案。 发现每次都要跳,很浪费,我们又只需要在failfail上跳(显然failfail所形成的是DAG),所以我们考虑用拓扑排序优化。

void qry(string t){
int u=0,len=t.length();
for (int i=0;i<len;i++){
u=T[u].son[t[i]-'a'];
T[u].ans++;
}
}
void topo(){
while (!Q.empty()) Q.pop();
for (int i=0;i<=cntn;i++){
if (!T[i].in)Q.push(i);
}
while (!Q.empty()){
int u=Q.front();Q.pop();
ans[T[u].id]=T[u].ans;
int v=T[u].fail;
T[v].ans+=T[u].ans;
if (!--T[v].in)Q.push(v);
}
}

AC自动机模板 ACAM完整代码

namespace ACAM {
struct tire{
int son[26],fail,in,id,ans;
void clear(){
fail=in=id=ans=0;
memset(son,0,sizeof(son));
}
}T[N];
int cntid,cntn,ans[N];
queue<int>Q;
void init(){
cntid=cntn=0;
T[0].clear();
}
int insert(string s){//return id;
int u=0,len=s.length();
for (int i=0;i<len;i++){
int& son=T[u].son[s[i]-'a'];//地址写法更方便
if (!son)son=++cntn,T[son].clear();
u=son;
}
if (!T[u].id)T[u].id=++cntid;//防止重复
return T[u].id;//记录编号
}
void build(){
//建立ac自动机
while (!Q.empty()) Q.pop();
for (int i=0;i<26;i++)
if (T[0].son[i])Q.push(T[0].son[i]);
while (!Q.empty()){
int u=Q.front();Q.pop();
for (int i=0;i<26;i++){
if (T[u].son[i]){
T[T[u].son[i]].fail=T[T[u].fail].son[i];
T[T[T[u].fail].son[i]].in++;
Q.push(T[u].son[i]);
}
else T[u].son[i]=T[T[u].fail].son[i];//改变树的结构
}
}
}
void qry(string t){
int u=0,len=t.length();
for (int i=0;i<len;i++){
u=T[u].son[t[i]-'a'];
T[u].ans++;
}
}
void topo(){
while (!Q.empty()) Q.pop();
for (int i=0;i<=cntn;i++){
if (!T[i].in)Q.push(i);
}
while (!Q.empty()){
int u=Q.front();Q.pop();
ans[T[u].id]=T[u].ans;
int v=T[u].fail;
T[v].ans+=T[u].ans;
if (!--T[v].in)Q.push(v);
}
}
}

ACAM上的DP#

String
https://fuwari.vercel.app/posts/string/
Author
Ender子浪
Published at
2025-11-28
License
CC BY-NC-SA 4.0