后缀数组
功能
将一个串ST所有后缀排序,并求出相邻排名后缀的LCP
SA, rank, 基数排序
倍增排序
为了将从i开始的后缀排序,我们倍增长度t=1,2,4,8…,即我们考虑从每个i开始长度为2k的段,如果不足2k就到结尾为止。长度为1的段直接比较即可。为了比较两个长度为2k的段,我们只需要比较前一半(长度为2(k-1)的段)和后一半。
因此,我们可以对于长度1,2,4,8…维护当前每一段的顺序,每次只需要利用前一次的结果进行双关键字排序即可。
我们每次调用std::sort,复杂度O(nlog2n)
基数排序优化
我们考虑对相等的元素排序会发生什么。我们发现,对于相等的元素,位置前面的会放到比较后面的位置!因为我们每次把前缀和减一,所以相等的元素会越来越前。
所以假设我们要对a和b双关键字排序,a优先。那么我们先按b排序,然后倒着循环对a排序,就可以实现双关键字排序了。
这样进行双关键字排序,每一轮就是O(nlogn)的。需要注意的是,每一轮后需要将相等的子串赋予相等的名次,不然后面没法比。
code
bool same(int x,int y,int z)
{
return t[x]==t[y]&&t[x+z]==t[y+z];
}
void get_SA()
{
for(int i=1; i<=n; i++) rk[i]=p[i],yjy[rk[i]]++;
for(int i=2; i<=m; i++) yjy[i]+=yjy[i-1];
for(int i=n; i>=1; i--) sa[yjy[p[i]]--]=i;
for(int j=1; j<=n; j<<=1)
{
int tot=0;
for(int i=n-j+1; i<=n; i++) tmpsa[++tot]=i;
for(int i=1; i<=n; i++) if(sa[i]>j) tmpsa[++tot]=sa[i]-j;
for(int i=1; i<=n; i++) tmprk[i]=rk[tmpsa[i]];
memset(yjy,0,sizeof(yjy));
for(int i=1; i<=n; i++) yjy[tmprk[i]]++;
for(int i=2; i<=m; i++) yjy[i]+=yjy[i-1];
for(int i=n; i>=1; i--) t[i]=rk[i],sa[yjy[tmprk[i]]--]=tmpsa[i];
m=0;
for(int i=1; i<=n; i++)
{
rk[sa[i]]=(i>1&&same(sa[i],sa[i-1],j))?m:++m;
}
++m;
}
for(int i=1; i<=n; i++) rk[sa[i]]=i;
}
height
就是相邻排名的LCP
我们设排好序的后缀开头为sa[1],sa[2]…sa[n],我们设height[i]为sa[i-1]和sa[i]的最长公共前缀长度,也就是要求的所谓排序后相邻后缀的最长公共前缀的长度。
我们假设我们已经有了height数组,我们考虑这个数组有什么用。对于任何两个后缀a,b,假设sa[i]=a,sa[j]=b,并且i<j,即a和b分别排名为i<j,我们可以证明它们的最长公共前缀就是min{height[i+1],height[i+2]…height[j]}。
接下来我们考虑如何求height数组,我们有如下性质,height[rank[i]]>=height[rank[i-1]]-1,即i开头的后缀对应的height至少是i-1开头的后缀对应的height-1。
这个结论其实很容易理解,你让i开头的后缀匹配i-1对应height的下一个字符,就可以取到height-1。
因此,我们可以按照顺序依次求height,每次把height[rank[i]]初始化为max(height[rank[i-1]]-1,0),并暴力匹配递增,复杂度显然是线性的。
code
void get_h()
{
int k=0;
for(int i=1; i<=n; i++)
{
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(j+k<=n&&i+k<=n&&st[i+k]==st[j+k]) k++;
height[rk[i]]=k;
}
}
后缀自动机
后缀自动机(SAM)是存储某个串所有后缀的最小自动机,即这个自动机可以且尽可以接受这个串的所有后缀。
right集合
对于每个原串中的子串u,我们记right(u)为u在原串中的出现位置右端点集合,例如对于ababa,right(aba)={3,5}。
考虑每个SAM节点对应串的right,因为SAM需要恰好接受后缀,如果有两个对应串right不同,那么相当于有一个出现位置离结尾距离不同,那么接上那个后缀后就一个应该到达止状态一个不应该,矛盾。因此,每个SAM节点对应串的right应该相同。事实上,任意两个点对应串的right都应该两两不同,因为相当于当前匹配到的位置一样,所以后面的转移应该是本质相同的。
fail连接
这个fail构成了fail数
构造
后缀自动机可以采用增量法构造,即假设我们已经构造了S[1…i]的SAM,接下来在尾部插入S[i+1]。
我们记包含S[1…i]的节点是u,我们称u到根的fail树上的链为u的fail path,即S[1…i],S[2…i]…S[i,i]对应的节点。我们要新建一个点包含S[1…i+1],设这个点为z。
假设S[i+1]=c,最简单的情况是u的fail path上的点均没有c孩子,即c是一个没出现过的字符,这时候我们把路径上的点的c孩子均设为z,z的fail指向根。
假设u的fail path上第一个有c孩子的点是s,在s之后的点c孩子直接指向z即可。假设s对应的最长串是S[l…i](由于在u的fail path上),它的c孩子为p,那么显然p包含了串S[l…i+1],但是p可能会包含更长的串。
如果p恰好只包含了S[l…i+1],即maxlen[p]=maxlen[s]+1,那么我们直接将z的fail指向p就行了。
否则,情况可能是s=S[l…i]=S[a…b],然后s通过c边会到达S[o…b+1],S[o+1…b+1]…S[a,b+1],其中S[a,b+1]就是我们希望有的S[l…i+1],我们希望将z的fail指向S[l…i+1],而不是前面那一坨。
这相当于把p拆分
将p拆分出q,其中maxlen[q]=maxlen[p]+1,即恰好存储了S[l…i+1],将fail[p]和fail[z]指向q,fail[q]指向fail[p]。q的转移边与p相同。
继续遍历s的fail链,将原来c孩子是p的一段改为q。
code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+77;
struct P
{
int ch[26],fail,ml;
}sam[N<<1];
int las=1,tot=1,cnt,ls[N<<1],f[N<<1];
ll ans=0;
struct E
{
int to,next;
}e[N*2];
void add(int u,int v)
{
e[++cnt].to=v; e[cnt].next=ls[u]; ls[u]=cnt;
}
void add(int c)
{
int p=las,nowp=las=++tot;
sam[nowp].ml=sam[p].ml+1; f[nowp]=1;
while(p&&!sam[p].ch[c]) sam[p].ch[c]=nowp,p=sam[p].fail;
if(!p) sam[nowp].fail=1;
else
{
int q=sam[p].ch[c];
if(sam[q].ml==sam[p].ml+1) sam[nowp].fail=q;
else
{
int nowq=++tot; sam[nowq]=sam[q];
sam[nowq].ml=sam[p].ml+1;
sam[q].fail=sam[nowp].fail=nowq;
while(p&&sam[p].ch[c]==q) sam[p].ch[c]=nowq,p=sam[p].fail;
}
}
}
char s[N];
void dfs(int u)
{
for(int i=ls[u]; i; i=e[i].next)
{
int v=e[i].to;
dfs(v); f[u]+=f[v];
}
if(f[u]!=1) ans=max(ans,f[u]*1ll*sam[u].ml);
}
int main()
{
int len;
scanf("%s",s); len=strlen(s);
for(int i=0; i<len; i++) add(s[i]-'a');
for(int i=2; i<=tot; i++) add(sam[i].fail,i);
dfs(1);
printf("%lld",ans);
}