字符串(长期更新)
设字符串
字符串Hash
Border理论
KMP
border:若字符串
周期:对于整数
重点在于重复利用已经求出的信息来降低复杂度。
定义
显然若
现在考虑递推地求
可以证明这是,但我不会证。
那么得到
void getfail(){
fail[0]=fail[1]=0;
for(int i=2,j=0;s[i];++i){
while(j&&s[i]!=s[j+1]) j=fail[j];
if(s[i]==s[j+1]) j++;
fail[i]=j;
}
}
Z函数
也有人叫它扩展KMP,但其实和KMP没有太大关系。
对于字符串
类似KMP,我们同样递推,利用已经求出的信息降低复杂度。
根据定义,对于任意的
考虑维护最靠右的匹配段
计算
-
若
, ,那么就有 ,初始化后再暴力拓展。 -
若
,直接暴力拓展。
计算结束后更新
看起来很暴力,但分析一下可以知道是
-
时,暴力拓展一定使 变大。 -
时,暴力拓展也会让 变大。 -
时,不会暴力拓展。
由于
-
对于这道题luogu Z函数模板的第二问,用推导
函数的思路推一下即可。 -
循环移位后与原串比大小:首先想到
与 比大小只需比 后的第一个字符即可。循环移位可以将字符串复制一份接在后面,然后上 函数推即可。
Trie
是各种自动机的基础。
数据结构里也会讲。这里只讲Trie作为字典使用的情形。
板子:
inline void ins(string s,int v){
int u=0;
for(int i=0;s[i];++i){
int c=idx(s[i]);
if(!ch[u][c]){
memset(ch[sz],0,sizeof(ch[sz]));
ch[u][c]=sz++;
}
u=ch[u][c];
val[u]+=v;
}
}
其他操作是类似的。理解如何在Trie上跳即可。
一般用于检索字符串是否出现。其他应用见数据结构篇。
AC自动机
AC自动机结合了Trie的结构和KMP失配的思想,解决的是多模式串匹配问题。
步骤:
-
将所有模式串构成Trie
-
对每个Trie上的节点构造失配指针
。
之后就可以利用构建出的AC自动机解决各种问题。
这里的Trie的节点表示一种状态,状态是某个模式串的前缀(即从Trie上的节点走到根经过的前缀),边表示转移,即转移到在当前节点的状态下加上某个字符后的状态。记所有状态的集合为
的构建
对于状态
考虑对于当前节点
我们假设现在深度小于
-
若
存在,那么 。即在 后加上 的最长后缀就是在 后加上 。 -
若
不存在,就继续跳 。 -
若
跳到根之后也没找到,那么
以上只是构建
继续优化
对于
我们在统计答案时会沿着
实现:
void ins(char s[],int idx){
int u=0;
for(int i=0;s[i];++i){
if(!tr[u][s[i]-'a']) tr[u][s[i]-'a']=++sz;
u=tr[u][s[i]-'a'];
}
if(!val[u]) val[u]=idx;
rev[idx]=val[u];//或者换成其他操作
}
void build(){
queue<int> q;
for(int i=0;i<26;++i){
if(tr[0][i]) ind[0]++,q.push(tr[0][i]);
}
while(!q.empty()){
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],ind[tr[fail[u]][i]]++,q.push(tr[u][i]);
else tr[u][i]=tr[fail[u]][i];
}
}
}
void qry(char s[]){
int u=0;
for(int i=0;s[i];++i){
u=tr[u][s[i]-'a'];
cnt[u]++;//或者换成其他操作
}
}
void topu(){
queue<int> q;
for(int i=0;i<=sz;++i){
if(!ind[i]) q.push(i);
}
while(!q.empty()){
int u=q.front();
q.pop();
ans[val[u]]=cnt[u];
int v=fail[u];
cnt[v]+=cnt[u];//或者换成其他递推操作
if(!--ind[v]) q.push(v);
}
}
Manacher
可以求以位置
可以观察到一件事:奇回文串的回文中心为一个字符,偶回文串的回文中心为字符间的空隙。于是想办法统一奇偶回文子串。
我们可以在每两个字符的间隔中插入一个特殊字符(如#
,首尾也要插入)。并且在构造好后再次在首尾插入两个不同的特殊字符以防越界(如$/@
)。
观察到新串的
同Z函数一样考虑,维护最靠右的右端点
考虑当前要计算的回文中心
-
若
,暴力尝试拓展。 -
若
,由对称性可知 。初始化后再尝试暴力拓展。
同Z函数分析,时间复杂度
PAM
回文自动机高度压缩了字符串中所有回文子串的信息,维护了原串中所有本质不同的回文子串。
PAM的结构可以看作两棵树,但实际上是一棵。有两个根,分别是奇根
来说转移边,
来说
构建
构建PAM的复杂度是
有引理:长度为
证明一下,上数归。
所以PAM的结点数是线性的,转移是唯一的,于是
对每个结点要维护该结点对应回文子串长度
考虑增量法构建PAM。新增一个字符
考虑求出
来证明复杂度,除了跳
或许还有更好理解而不严谨的解释。每次加入一个字符后,最长回文后缀的长度至多
应用
本质不同回文子串个数
就是PAM的状态数
求每个回文子串在原串中的出现次数
考虑增量法构建的过程。在加入一个字符后,当前字符串的所有回文后缀的出现次数都要
求前一半是偶回文串,后一半也是偶回文串的回文子串
显然这种回文子串的长度为
定义
其他待补
SA
后缀数组。要实现的是后缀排序,就是把
一般来说都用倍增求SA,
于是学习倍增求SA就好。
倍增求SA
考虑依次比较后缀中的第
首先按第一个字符排序,即对原串中的每个字符排序。
然后开始倍增,考虑对
具体说一下基数排序的过程。
这里
首先根据
然后开始倍增,枚举
现在来看每一轮的过程。
首先求出
然后再进行基数排序,还是按
接下来
首先对于
然后从
算好
注意到判断中
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,m,p,rk[maxn],sa[maxn],id[maxn],buc[maxn];
char s[maxn];
void build(){
n=strlen(s+1);
m=128,p=0;
for(int i=1;i<=n;++i) buc[rk[i]=s[i]]++;
for(int i=1;i<=m;++i) buc[i]+=buc[i-1];
for(int i=n;i;--i) sa[buc[rk[i]]--]=i;
for(int w=1;;w<<=1,m=p,p=0){
for(int i=n-w+1;i<=n;++i) id[++p]=i;
for(int i=1;i<=n;++i) if(sa[i]>w) id[++p]=sa[i]-w;
for(int i=1;i<=m;++i) buc[i]=0;
for(int i=1;i<=n;++i) buc[rk[i]]++;
for(int i=1;i<=m;++i) buc[i]+=buc[i-1];
for(int i=n;i;--i) sa[buc[rk[id[i]]]--]=id[i],id[i]=rk[i];
rk[sa[1]]=1,p=1;
for(int i=2;i<=n;++i) rk[sa[i]]=(id[sa[i]]==id[sa[i-1]]&&id[sa[i]+w]==id[sa[i-1]+w])?p:++p;
if(p==n) break;
}
return;
}
int main(){
scanf("%s",s+1);
build();
for(int i=1;i<=n;++i) printf("%d ",sa[i]);
return 0;
}
SA与LCP
定义
首先,有显然的式子
然后就是不那么显然的式子:
证明一下:设
假设
再来一个不那么显然的式子:
证明一下,通过上一个式子可知
令
再令
最后一个引理:
证明一下,先把两边写开,
于是求
以上三个式子是SA中很重要的部分,还请熟记。
应用
求两个后缀的LCP
直接求出
比较两个子串的大小
设
先求出
否则
本质不同子串个数
可以考虑用所有子串数目
子串可以视作后缀的前缀,于是按
其实用增量的想法也可以导出答案,就是每次新增的量加在一起,于是答案为
其他待补
后缀树
AC自动机中文本串未知而模式串已知,而后缀树与之相反,文本串已知而模式串未知,我们需要利用某种东西维护文本串所有子串的信息。
首先可以有暴力的想法,我们把文本串所有的后缀都扔进Trie。然而这样结点个数是
我们在每个后缀的最后加上一个特殊的结点(不妨叫它后缀结点)记录后缀的开始位置,然后建立Trie的关于
这棵树的性质很好。可以发现在给出一个模式串后,把它扔到这棵树上类似AC自动机一样跳,那么它可以匹配到的位置就是它最终跳到的结点的子树内的所有叶子结点记录的位置。在这棵树上DFS就可以得到后缀数组。求
构建这棵树肯定不能直接插入然后建虚树,于是有了SAM。
SAM
SAM是一个能识别
-
是DAG,结点为状态,边为转移,转移上带一个字符(与AC自动机和PAM一样)。
-
存在一个初始状态,从这里出发可以到达每一个结点,所经转移上的字符写下来是
的一个子串。 的每个子串都可以这样表示出来。 -
存在若干终止状态,从初态出发到任意一个终态都可以得到
的一个后缀。 的每个后缀都可以这样表示出来。 -
状态数和转移数都是线性的。
构建
定义
可以发现
继续发掘
引理1:对于字符串的任意子串
引理2:对于字符串的任意子串
由以上两条,启示我们可以将
引理3:对于任意一个
现在我们设
这棵树被称作Parent Tree,实际上这棵树与后缀树的区别在于这棵树的转移是在前面加字符,于是这棵树可以视作反串的后缀树。根据分割关系可知,总结点数是
我们记等价类
但这还不是SAM,SAM的转移边应该是在子串的后面加上一个字符。我们希望在Parent Tree上增加上一些转移边使SAM满足其性质。
构建
考虑增量法构建SAM,新增一个字符后维护新增的子串(以新增字符结尾的子串)。
设当前加入的字符在第
首先,出现了新的一个最大的子串
接下来考虑如何修改已有节点的
否则现在跳到了
如果
否则,我们需要将
拆分完后,考虑对其他状态的转移的影响,一些指向
最后将
复杂度的证明:首先状态数就是Parent Tree的结点数,是
然后来看构造部分的时间复杂度。对于跳
应用
判断子串
把文本串扔到SAM上,看能否找到对应节点。
访问后缀
直接从一个结点开始跳
求
由Parent Tree的分割关系,可以在Parent Tree上线段树合并求出一个结点的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】