字符串(长期更新)

设字符串s长度为n,以下简记pres,i=s1i,sufs,i=sni+1n

字符串Hash

Hash

Border理论

Border理论

KMP

border:若字符串s的真前缀pre与真后缀suf满足pre=suf,则称之为s的一个border。

周期:对于整数p=1,2,3,,|s|,若对于i[1,|s|p]都有si=si+p,就称ps的周期。

重点在于重复利用已经求出的信息来降低复杂度。

定义faili表示前缀pres,i的最长border的长度。

显然若ts的border,那么t的border也是s的border。

现在考虑递推地求faili。观察到pres,i的最长的border是pres,i1的某一个border加上si。那么就可以想到不断跳pres,i1的border并检查加上si后是否为pres,i的border。这样就求出了faili

可以证明这是O(n),但我不会证

那么得到faili后做各种事情就很方便了。

faili也告诉了我们第i位失配后应该跳到哪里。

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没有太大关系。

对于字符串s,定义zi表示sufs,is的最长公共前缀(LCP)的长度。我们可以O(n)求出zi

类似KMP,我们同样递推,利用已经求出的信息降低复杂度。

根据定义,对于任意的i都有s1zi=sii+zi1。这类似于border,可以用来加速。

考虑维护最靠右的匹配段sii+zi1,记作[l,r]。特别地,z1=|s|没有意义,不算入匹配段中。于是从2开始,初始化l=r=0

计算zi时,讨论一下:

  • irs1rl+1=slrsil+1,rl+1=si,r,那么就有zimin(ri+1,zil+1),初始化后再暴力拓展。

  • i>r,直接暴力拓展。

计算结束后更新[l,r]即可。

看起来很暴力,但分析一下可以知道是O(n)的:

  • i>r时,暴力拓展一定使r变大。

  • irzil+1>ri+1时,暴力拓展也会让r变大。

  • irzil+1ri+1时,不会暴力拓展。

由于r最多从0变大到|s|,所以复杂度是O(n)的。

  • 对于这道题luogu Z函数模板的第二问,用推导z函数的思路推一下即可。

  • 循环移位后与原串比大小:首先想到st比大小只需比LCP(s,t)后的第一个字符即可。循环移位可以将字符串复制一份接在后面,然后上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上跳即可。ch[u][i]表示u节点经过代表字符i的边后到达的儿子节点。

一般用于检索字符串是否出现。其他应用见数据结构篇。

AC自动机

AC自动机结合了Trie的结构和KMP失配的思想,解决的是多模式串匹配问题。

步骤:

  1. 将所有模式串构成Trie

  2. 对每个Trie上的节点构造失配指针fail

之后就可以利用构建出的AC自动机解决各种问题。

这里的Trie的节点表示一种状态,状态是某个模式串的前缀(即从Trie上的节点走到根经过的前缀),边表示转移,即转移到在当前节点的状态下加上某个字符后的状态。记所有状态的集合为Q

fail的构建

对于状态ufailu=v,使得vQvu的最长后缀。

考虑对于当前节点u,设其父亲为pp通过字符c的边指向utr[p,c]=u

我们假设现在深度小于u的节点的fail都已经求得。(深度小于u即状态的长度小于u,这是真正要限定的。)

  • tr[failp,c]存在,那么failu=tr[failp,c]。即在p后加上c的最长后缀就是在failp后加上c

  • tr[failp,c]不存在,就继续跳fail

  • fail跳到根之后也没找到,那么failu=0

以上只是构建fail的基本思想。实际运用时,我们要对fail做路径压缩,即若tr[u,c]不存在,那么tr[u,c]=tr[failu,c]。这样修改Trie的结构后,我们在跳边的时候可能会丢失前缀,但没关系,后缀能匹配那么原串就一定能匹配,并且fail其实也是在丢失前缀。

继续优化

对于fail连成的边,有一个性质:在AC自动机中只保留fail边,一定构成树。这是因为fail一定不成环,且深度比当前点小。

我们在统计答案时会沿着fail不断跳,那么原问题就变成了树上的链求和问题,可以拓扑排序优化建图,最后统计答案时按拓扑序递推过去。(不用真建fail树,只需记录入度。)

实现:

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

可以求以位置i为中心的最长回文子串(其半径也等于以位置i为中心的回文子串个数。)

可以观察到一件事:奇回文串的回文中心为一个字符,偶回文串的回文中心为字符间的空隙。于是想办法统一奇偶回文子串。

我们可以在每两个字符的间隔中插入一个特殊字符(如#,首尾也要插入)。并且在构造好后再次在首尾插入两个不同的特殊字符以防越界(如$/@)。

观察到新串的Ri表示原串的对应回文中心的最长回文串长度加1

同Z函数一样考虑,维护最靠右的右端点r及其对应回文中心c,记回文半径为Rc

考虑当前要计算的回文中心i

  • i>r,暴力尝试拓展。

  • ir,由对称性可知Rimin(ri+1,R2ci)。初始化后再尝试暴力拓展。

同Z函数分析,时间复杂度O(n)

PAM

回文自动机高度压缩了字符串中所有回文子串的信息,维护了原串中所有本质不同的回文子串。

PAM的结构可以看作两棵树,但实际上是一棵。有两个根,分别是奇根odd和偶根evenodd上连着所有长度为奇数的回文子串,自身长度置为1even上连着所有长度为偶数的回文子串,自身长度置为0

来说转移边,u的一条c的转移边指向v,表示svsu的前后都加上c所形成的回文串。特别的,odd连出来的边指向长度为1的回文子串。

来说fail边,对每个结点ufailu指向它的最长回文后缀所代表的结点。显然fail边形成了类似树的结构。evenfail边指向oddoddfail边指向自身。

构建

构建PAM的复杂度是O(n)的。

有引理:长度为|s|的字符串s的本质不同回文子串至多有|s|个。

证明一下,上数归。|s|=1时显然成立。设已有的字符串s=t+c,假设该结论对t成立。设以c为右端点的回文子串的左端点分别为l1,l2lk,设li<li+1。于是在回文子串sl1|s|中用一下回文的性质,可以发现以l2,l3lk为左端点且以c为右端点的回文子串已经在t中出现过,于是s中至多比t中新增一个本质不同回文子串。于是得证。

所以PAM的结点数是线性的,转移是唯一的,于是O(n)

对每个结点要维护该结点对应回文子串长度lenfail边。

考虑增量法构建PAM。新增一个字符c,由以上引理,每次插入一个字符最多新增一个结点,即当前字符串的最长回文后缀。记last为上次插入后最长回文后缀对应的点。设新增的结点为u,当前下标为i,那么ux转移过来,当且仅当xlast的一个回文后缀,且silenx+1=cx可以通过从last开始不断跳fail边来找到。这时要判断一下xc转移边指向的点是否已经存在,如果还没有,那就新建结点,lenu=lenx+2

考虑求出ufail。可以发现ufail一定是x的一个回文后缀在前后加上字符c。如果x为奇根,那么当前字符串的最长回文后缀为c,那么ufail直接连到0。否则就从failx开始跳,设现在跳到的点为y,直到sileny+1=c,那么此时的yc转移边指向的结点就是ufail。显然这个点是已经建立过的,不必新建结点,因为由于x的回文性质且x的前后都为c,那么yx中对应的与之回文的前缀一定也满足前后都为c,而这个点在PAM中已经有了。

证明复杂度,除了跳fail的过程,其他显然是O(n)的。于是来分析跳fail的过程,上fail树。这里将oddeven视作深度为0。每次新加一个结点,在fail树上的动作表现为:先不断跳父亲,然后连边新建结点。跳一次父亲使深度1,新建一个结点使深度+1,然后使用势能分析(?)或者人类智慧(?)可以知道跳fail的总次数不会超过2n,所以是O(n)的。

或许还有更好理解而不严谨的解释。每次加入一个字符后,最长回文后缀的长度至多+2,每次跳fail至少使最长回文后缀的长度1,总共加入n次字符,所以至多跳2nfail

应用

本质不同回文子串个数

就是PAM的状态数2(减去oddeven)。

求每个回文子串在原串中的出现次数

考虑增量法构建的过程。在加入一个字符后,当前字符串的所有回文后缀的出现次数都要+1,于是直接在最长回文后缀对应结点上打上tag,最后按拓扑序推平就好。而PAM的构建中本就有拓扑序(一个点的fail的编号肯定比它自己小),于是在PAM的结点中按编号从大到小扫一遍推平就行。

求前一半是偶回文串,后一半也是偶回文串的回文子串

显然这种回文子串的长度为4的倍数,但这个性质没什么用。

定义trans指针,指向长度不超过当前节点长度的一半的结点。那么此题就是要找trans指向的结点的长度恰好为当前节点长度的一半的结点。维护trans的手法与fail类似,若fail2,那么trans=fail。否则设当前节点为ux经过c转移边指向u,那么从transx开始跳fail找到符合的结点即可。

其他待补

SA

后缀数组。要实现的是后缀排序,就是把s的所有后缀排序。包含两个数组(或者可以视作映射):sai,第i小的后缀在原串中的开始位置;rki,在原串中开始位置为i的后缀(以下称之为后缀i)的排名。这两个互为反函数,复合一下就消了:sarki=rksai=i

一般来说都用倍增求SA,O(nlogn),在OI中已经够用了。有O(n)的求法,但不好写,所以一般不会卡O(nlogn)的写法。

于是学习倍增求SA就好。

倍增求SA

考虑依次比较后缀中的第1,2,32k位。

首先按第一个字符排序,即对原串中的每个字符排序。

然后开始倍增,考虑对k=n排序之后,对k=2n时排序。将每个子串拆成前2k1位和后2k1位,于是就是双关键字排序,使用基数排序即可一次O(n)排序,再加上倍增就是O(nlogn)

具体说一下基数排序的过程。

这里rki尚有重复值,于是可以视作每个后缀的权值。

m表示rk的值域。

首先根据rk求出初始的sabuci表示rk=i的后缀的个数,做前缀和之后就表示rki的后缀的个数。然后考虑往sa中填数,相同权值的后缀我们不关心其内部排名,于是直接令位置靠后的排名较大即可。

然后开始倍增,枚举w,每轮倍增之后更新一下rk的值域,是一点常数优化。

现在来看每一轮的过程。

首先求出idi表示第二关键字排名为i的后缀的开始位置。显然开始位置在[nw+1,n]之间的后缀,其第二关键字为空,肯定靠前,于是直接加入。对于其余后缀,直接枚举第二关键字的排名i,然后sai就得到了第二关键字的起始位置,再saiw就是整个的起始位置。

然后再进行基数排序,还是按rk放到桶里。这次权值相同的不能随便排序,而是要按第二关键字的大小排序。于是我们从大到小枚举第二关键字,这样先枚举到的第二关键字更加靠后,于是给这个子串更大的排名。一开始把rk扔到桶里已经保证了第一关键字是有序的,于是不用管了。

接下来sa已经求好了,我们要利用sa重新求出rk。我们会用到之前的rk,于是可以用idrk保存下来。

首先对于sa1,根据反函数的性质,rksa1=1,并且利用p来记录rk当前的值域,现在p=1

然后从2开始,扫各个sa。如果后缀sai与后缀sai1的权值相同,并且后缀sai+w与后缀sai1+w的权值相同,那么现在这两个后缀的权值就相等,rksai=rksai1=p。否则就不相等,那么sai的权值比sai1的权值大1,所以rksai=rksai1+1=p+1,然后pp+1

算好rk后,若值域为n,那么每个位置的rk互不相同,就可以结束了。

注意到判断中sai+w的值域其实是2n的,但是rkid只用开到n+1。因为若saisai1的权值不同,就不会进后面那个判断,若saisai1的权值相同,则[sai,sai+w1][sai1,sai1+w1]相同,那么sai+wn+1sai1+wn+1

#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

定义LCP(i,j)表示后缀saisaj的最长公共前缀的长度。

首先,有显然的式子LCP(i,j)=LCP(j,i)以及LCP(i,i)=nsai+1

然后就是不那么显然的式子:1ikjn,LCP(i,j)=min{LCP(i,k),LCP(k,j)}

证明一下:设t=min{LCP(i,k),LCP(k,j)},那么LCP(i,k)tLCP(k,j)t。后缀sai的前t位与sak的前t位相同,sak的前t位与saj的前t位相同,于是sai的前t位与saj的前t位相同。所以LCP(i,j)t

假设LCP(i,j)=r>t,那么后缀sai的前r位与saj的前r位相同,于是sai的前t+1位与saj的前t+1位也必定相同。由t的定义以及ikj,可以得到后缀sai的第t+1位不大于sak的第t+1位,且saj的第t+1位不小于sak的第t+1位,且两边不能同时取得等号,所以sai的第t+1位不等于saj的第t+1位,与假设矛盾,所以原命题得证。

再来一个不那么显然的式子:LCP(i,j)=mini<kj{LCP(k,k1)}

证明一下,通过上一个式子可知LCP(i,j)=min{LCP(i+1,i),LCP(i+1,j)},然后继续拆后面那一项即可证明。或许可以用数归写成更严谨的形式。总之证完了。

heighti=LCP(i,i1),并且height1=0,那么求出height之后LCP就是区间求min

再令hi=heightrki,那么heighti=hsai

最后一个引理:hihi11

证明一下,先把两边写开,heightrkiheightrki11,也即LCP(rki,rki1)LCP(rki1,rki11)1。设x=sarki1,y=sarki11。那么就是要证LCP(rki,rkx)LCP(rki1,rky)1。如果后缀i1y的首字母不同,那么显然成立。如果首字母相同,那么去掉首字母后可以得到后缀y+1i,由于rky<rki1,故而rky+1<rki,并且LCP(rky+1,rki)=LCP(rki1,rky)1。又rkx=rki1,所以rkk<rki,LCP(rki,rkx)LCP(rki,rkk)。那么综上,LCP(rki,rkx)LCP(rki,rkk+1)=LCP(rki1,rky)1,即原命题成立,hihi11

于是求hi时,最多进行n1,于是最多往后扫2n次,所以是O(n)的。

以上三个式子是SA中很重要的部分,还请熟记。

应用

求两个后缀的LCP

直接求出height,然后应用第二个式子(不妨rki<rkj):LCP(rki,rkj)=minrki<krkj{heightk},就是RMQ,随便做。

比较两个子串的大小

A=s[ab],B=s[cd]

先求出L=LCP(rka,rkc),若L=min(|A|,|B|),则A<B|A|<|B|

否则A<Brka<rkc

本质不同子串个数

可以考虑用所有子串数目n(n+1)2减去重复的。

子串可以视作后缀的前缀,于是按rk递增的顺序枚举每个后缀,那么对于当前后缀,新增的部分就是整个长度减去与上一个前缀的LCP的差。于是重复的部分就是2knheightk,答案就是n(n+1)22knheightk

其实用增量的想法也可以导出答案,就是每次新增的量加在一起,于是答案为n(n+1)21knheightk,而height1=0

其他待补

后缀树

AC自动机中文本串未知而模式串已知,而后缀树与之相反,文本串已知而模式串未知,我们需要利用某种东西维护文本串所有子串的信息。

首先可以有暴力的想法,我们把文本串所有的后缀都扔进Trie。然而这样结点个数是O(n2)的,我们需要继续压缩信息。

我们在每个后缀的最后加上一个特殊的结点(不妨叫它后缀结点)记录后缀的开始位置,然后建立Trie的关于n个后缀结点的虚树(这是压缩信息的利器),就得到了后缀树。

这棵树的性质很好。可以发现在给出一个模式串后,把它扔到这棵树上类似AC自动机一样跳,那么它可以匹配到的位置就是它最终跳到的结点的子树内的所有叶子结点记录的位置。在这棵树上DFS就可以得到后缀数组。求LCP就是求LCA的深度。

构建这棵树肯定不能直接插入然后建虚树,于是有了SAM。

SAM

SAM是一个能识别s的所有后缀的最小DFA,有以下性质:

  • 是DAG,结点为状态,边为转移,转移上带一个字符(与AC自动机和PAM一样)。

  • 存在一个初始状态,从这里出发可以到达每一个结点,所经转移上的字符写下来是s的一个子串。s的每个子串都可以这样表示出来。

  • 存在若干终止状态,从初态出发到任意一个终态都可以得到s的一个后缀。s的每个后缀都可以这样表示出来。

  • 状态数和转移数都是线性的。

构建

定义endpos(t)表示ts中所有结束位置的集合。上文后缀树就是将endpos相同的结点合并在一起,SAM也可以利用这种思想来压缩信息。

可以发现endpos相同的子串有很多,并且对endpos相同的子串后面加上一个相同的字符,所得到的新子串的endpos仍然相同。于是可以对endpos等价类(就是endpos相同的子串的集合)建立SAM,一种转移只会对应一个状态,当然一个状态可以通过多种转移得到。

继续发掘endpos的性质。

引理1:对于字符串的任意子串uv,若endpos(u)=endpos(v)|u|<|v|,那么uv的后缀。这一条是显然的。

引理2:对于字符串的任意子串uv,若|u||v|,那么endpos(v)endpos(u)要么包含(endpos(v)endpos(u))要么无交。证明很简单,若uv的子串,那么u出现是v出现的必要不充分条件;否则uv一定不会同时出现。

由以上两条,启示我们可以将s的所有子串按endpos划分为若干等价类。

引理3:对于任意一个endpos等价类,其中的子串按大小排序,则一定是后一个子串是前一个子串的后缀,且长度减一,长度在值域上是连续的一段。证明很简单,设u是最短的,v是最长的。当u=v时一定成立,否则由引理1,uv的子串,所以对于v的所有长度|u|的后缀,由第二条性质,一定在这个等价类中。

现在我们设v是一个等价类中最长的一个子串,在v的前面加上一个字符后形成的新子串的endpos一定是endpos(v)的子集。而且显然的,在v的前面加上不同的字符会指向不同的endpos等价类,且这些等价类两两无交。于是我们相当于把这个endpos分割成若干部分。借这个关系可以建出一棵树。注意这棵树上的转移边表示在endpos等价类中的最长子串前面加上一个字符所得到的endpos

这棵树被称作Parent Tree,实际上这棵树与后缀树的区别在于这棵树的转移是在前面加字符,于是这棵树可以视作反串的后缀树。根据分割关系可知,总结点数是O(n)的。

我们记等价类x中最长子串的长度为lenx,最短子串长度为minlenx,在Parent Tree上的父亲为linkx,于是由以上过程可知minlenx=lenlinkx+1,因为x中的最短的一个是linkx中最长的一个在前面加上一个字符得到的。

但这还不是SAM,SAM的转移边应该是在子串的后面加上一个字符。我们希望在Parent Tree上增加上一些转移边使SAM满足其性质。

构建

考虑增量法构建SAM,新增一个字符后维护新增的子串(以新增字符结尾的子串)。

设当前加入的字符在第i位,字符为c,第1位到第i1位构成的字符串所在等价类的编号为x。设初始节点(代表着空串)的编号为1

首先,出现了新的一个最大的子串s1i,于是新建一个点cur,并且有lencur=lenx+1

接下来考虑如何修改已有节点的endpos。发现需要修改endpos的点都是s1i1的一个后缀通过c转移边指向的点。跳后缀只需从x开始不断跳link就好。可以发现这是在从大到小遍历后缀,所以一开始较大的后缀可能没有c转移边,也就是说这些后缀在后面加上c形成的子串没有在s1i1中出现过,于是这些后缀通过c转移边直接指向cur。如果这样一直跳到了根,那么cur在Parent Tree上的父亲就是1

否则现在跳到了pp经过一条c转移边指向了q。那么现在p中最大的子串就是s1i1的一个后缀,那么q中长度不大于lenp+1的子串就是s1i的一个后缀,这些子串的endpos中新增了i

如果lenq=lenp+1,那么是好处理的,整个qendpos都新增了i,于是不用动q,并且将linkcur=q

否则,我们需要将q分裂。设q分裂出来的节点为npnp中的所有子串的长度都不大于lenp+1,那么np中最长的子串就是s1i的后缀,于是lennp=lenp+1np的转移还是和q的转移是一样的,于是直接拿过来。来考虑linknp,原来linkq通过在前面加上一个字符得到了原来q中最短的子串,而这个子串一定在np中,所以linknplinkq,然后linkqnp

拆分完后,考虑对其他状态的转移的影响,一些指向q的转移应该指向np。从p开始继续跳link(这相当于在前面减去一些字符),跳到新的p,如果能通过c转移边指向q,那么就将这个转移改到np上。如果指向的不是q,那就是指向的q在Parent Tree上的祖先,整个的endpos都要增加i,就不用改了。

最后将xcur,就构建好了。

复杂度的证明:首先状态数就是Parent Tree的结点数,是O(n)的。现在来证转移数是O(n)的。我们找到SAM的一棵生成树,从一个终止结点开始走转移边跑到初始结点来找到对应的后缀。当走到一条非树边时,走过去,然后沿生成树上的路径走回初始状态。这样走出来的不一定是对应的后缀,但一定也是一个后缀,并且没有跑出来过。每次跑出来一个后缀,就把这个后缀划去,然后重复以上过程。因为后缀数量为n,于是非树边的数量为O(n),然后树边的数量与状态数相关,也是O(n)的,于是转移数就是O(n)的。

然后来看构造部分的时间复杂度。对于跳xlink连向cur的转移和将q的转移拿到np上,都是增加了转移,而转移数是O(n)的,于是总的是O(n)的。对于跳plink修改到q的转移,可以发现每条边只会被修改一次,因为已经满足了lennp=lenp+1,下一次遍历到它时不会进行修改转移的遍历,又因为边数是O(n)的,所以还是O(n)的,于是构造SAM的时间复杂度是O(n)的。

应用

判断子串

把文本串扔到SAM上,看能否找到对应节点。

访问后缀

直接从一个结点开始跳link

endpos

由Parent Tree的分割关系,可以在Parent Tree上线段树合并求出一个结点的endpos

广义SAM

基本子串结构

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