前言
只是一个小记,不是算法详解
参考资料
史上最通俗的后缀自动机详解
广义SAM模板题解
正题
概念
定义
简单的,一个有向无环图,边有字母,满足起点开始的每一条路径都是原串的一个子串。
并且保证复杂度在O(n)O(n)O(n)级别内的。
endposendposendpos相关
每一个子串ppp的endpos(p)endpos(p)endpos(p)被定义为原串中所有出现子串ppp的末尾位置集合。
endposendposendpos相同的子串被定义为一个endposendposendpos类,可以证明任何一个串的endposendposendpos类数量不会超过O(n)O(n)O(n)级别。
性质:
- 对于两个endposendposendpos相同的子串,其中短的一个一定是另一个的后缀
- 对于任意两个子串p,qp,qp,q,且qqq的长度大于ppp,一定有endpos(p)⊆endpos(q)endpos(p)\subseteq endpos(q)endpos(p)⊆endpos(q)或endpos(p)∩endpos(q)=∅endpos(p)\cap endpos(q)=\varnothingendpos(p)∩endpos(q)=∅
parentsparentsparents树
根据endposendposendpos的两个性质我们可以构建一棵树,其中对于两个endposendposendpos类x⊆yx\subseteq yx⊆y那么就有一条边x−>yx->yx−>y。
性质:
- 对于一个在faxfa_xfax的endposendposendpos类里的串一定是xxx的endposendposendpos类串的后缀
- 定义在faxfa_xfax的endposendposendpos类里的最长的串长度为LenLenLen,在endposendposendpos类里最短的串长度为lenlenlen那么有Len=len+1Len=len+1Len=len+1
还有一个最重要性质,就是parentsparentsparents里的节点就是SAMSAMSAM里的节点
代码解析
懒得搞图,图片去看参考资料。
void add(int c){int p=las;int np=las=++cnt;len[np]=len[p]+1;siz[cnt]++;for(;p&&!ch[p][c];p=fa[p])ch[p][c]=np;if(!p)fa[np]=1;else{int q=ch[p][c];if(len[p]+1==len[q])fa[np]=q;else{int nq=++cnt;len[nq]=len[p]+1;memcpy(ch[nq],ch[q],sizeof(ch[nq]));fa[nq]=fa[q];fa[np]=fa[q]=nq;for(;p&&ch[p][c]==q;p=fa[p])ch[p][c]=nq;}}return;
}
- 第333行:新节点的最长一定新串的长度,因为左右不可能再加任何节点。
- 第444行:往前跳,中途指一条ccc的边向新节点npnpnp(更新路径上的endposendposendpos)。但是如果一个位置已经有这条边了,前面已经加入过这个字符即有这些新的endposendposendpos了。在parentparentparent树上跳的原因是因为这条链上的endposendposendpos是包含后缀的。
- 第555行:如果字符ccc是新出现的字符,那么这一定是一个由根节点印出来的新endposendposendpos类。
- 第888行:定义新的ppp为p′p'p′,因为p′p'p′是ppp的祖先,且p′p'p′中最长的长度加一是ppp中最长的,那么也就是qqq中最长的与npnpnp拥有相同的endposendposendpos即endpos(np)⊆endpos(q)endpos(np)\subseteq endpos(q)endpos(np)⊆endpos(q)那么npnpnp的父节点就是qqq
- 第10∼1310\sim 1310∼13行:这是SAMSAMSAM中最关键的部分,当lenp+1len_p+1lenp+1不等于lenqlen_qlenq时,那么表示lenqlen_qlenq中不能所有的endposendposendpos都加入nnn,而是有一部分要分离出来。此时我们需要新建立一个节点nqnqnq来储存加入了nnn这个endposendposendpos的endposendposendpos类。首先加入新字符之前nqnqnq与qqq完全相同,所以我们可以直接继承qqq的信息。而因为nqnqnq是可以加入nnn这一信息的,所以有endpos(q)⊆endpos(nq)endpos(q)\subseteq endpos(nq)endpos(q)⊆endpos(nq)所以我们让qqq的父节点为nqnqnq,然后显然npnpnp的父节点也为nqnqnq。然后我们要往前更新信息,如果跳到chp,c≠qch_{p,c}\neq qchp,c=q时,这个节点ppp本身ccc的出边时本身就与qqq没有关系的,所以就可以不用再更新了。
广义SAM
如果一个SAMSAMSAM中我们要加入多个字符串时我们应该怎么办,广义SAMSAMSAM分为离线的和在线的做法。
离线做法:对于所有的字符串我们先构造出一棵TrieTrieTrie树,然后每一次的lastlastlast就是它TrieTrieTrie树上的父节点的节点,因为这样保证了一个节点后面不会插入重复的字符。
在线做法:考虑如果插入了重复的字符怎么办,那么证明这个节点的endposendposendpos类一定是有出现过的,我们可以像SAMSAMSAM中后面那个判断一样搞就好了
这里是在线做法的代码:
ll Ins(ll c,ll last){ll p=last;if(ch[p][c]){ll q=ch[p][c];if(len[p]+1==len[q])return q;else{ll nq=++cnt;len[nq]=len[p]+1;memcpy(ch[nq],ch[q],sizeof(ch[nq]));fa[nq]=fa[q];fa[q]=nq;for(;p&&ch[p][c]==q;p=fa[p])ch[p][c]=nq;return nq;}}ll np=++cnt;len[np]=len[p]+1;for(;p&&!ch[p][c];p=fa[p])ch[p][c]=np;if(!p)fa[np]=1;else{ll q=ch[p][c];if(len[p]+1==len[q])fa[np]=q;else{ll nq=++cnt;len[nq]=len[p]+1;memcpy(ch[nq],ch[q],sizeof(ch[nq]));fa[nq]=fa[q];fa[q]=fa[np]=nq;for(;p&&ch[p][c]==q;p=fa[p])ch[p][c]=nq;}}return np;
}
例题解析
P3804-[模板]后缀自动机
给出一个长度为nnn的串,求一个最大的子串长度乘上子串出现次数
在构造SAMSAMSAM的时候统计每个endposendposendpos类包含了几个endposendposendpos,这就是这个endposendposendpos类子串中出现的次数,然后每个endposendposendpos类取最长的子串就好了(在构造时已经保存了)。
对于每个endposendposendpos的集合如何统计,对于每个npnpnp,它都会有一个前缀iii作为这个endposendposendpos类,而在往下的过程中这个前缀位置iii会丢失(因为再往下的末尾都是在iii之后,而没有一个在iii的后缀是在之后加入的)。也就是让fnp++f_{np}++fnp++,然后按照长度从大到小让ffax+=fxf_{fa_x}+=f_{x}ffax+=fx,因为一个节点的父节点长度一定比子节点小。
P3975-[TJOI2015]弦论
求一个字符串第kkk大的子串。
对于相同的子串算同一个时,那么其实就是求SAMSAMSAM上字典序第kkk大的路径,处理出每一个节点的后继状态直接跑即可。
对于相同的子串不算同一个时,我们处理处每个节点的endposendposendpos集合大小,这就是这个endposendposendpos类中每个子串的出现次数。然后用这个处理后继状态直接跑即可。
SP1811-Longest Common Substring
求两个串的最长公共子串
我们先构建第一个串的SAMSAMSAM,然后用第二个串在第一个串上跑,如果不能继续跑下去那么我们直接在parentparentparent树上往前跳就好了,因为在SAMSAMSAM中parentparentparent树的左右可以类似于ACACAC自动机中failfailfail树的定义。然后跳完了之后取len+1len+1len+1就好了。
P5341-[TJOI2019]甲苯先生和大中锋的字符串
求出现次数恰好为kkk的字符串中出现次数最多的长度
因为对于一个相同的endposendposendpos类中的字符串都是长度连续且不同的,也就是[lenfax+1,lenx][len_{fa_x}+1,len_x][lenfax+1,lenx]这个范围内每一个长度各有一个字符串,然后差分做就好了。