后缀数组
\(sa\) 和 \(rk\) 数组
定义
\(sa_i\) 表示将所有后缀按字典序排序后第 \(i\) 小的后缀以 \(sa_i\) 开头, \(rk_i\) 表示以 \(i\) 开头的后缀的排名。
有性质 \(sa_{rk_i}=rk_{sa_i}=i\) 。
求法
用倍增优化暴力做法。
设 \(rk_{w,i}\) 表示以 \(i\) 开头的长度为 \(w\) 的串在所有长度为 \(w\) 的串中排名为多少。
那么,以 \(rk_{w,i}\) 为第一关键字, \(rk_{w,i+w}\) 为第二关键字排序,即可求出 \(rk_{2w}\) 。
把字符串中每个字符排序,得到 \(rk_1\) 后即可推出 \(rk\) 数组。
用 sort 排序,复杂度为 \(O(n \log^2 n)\) 。
因为这个排序的值域为 \(O(n)\) ,考虑用基数排序代替 sort 的部分,复杂度为 \(O(n \log n)\) 。
但是,第二关键字的排序其实并不需要计数排序。只需把空串放在前面,其它串按原顺序排好即可。
这里就放一份用 sort 实现后缀排序的代码,比较清晰,方便理解。把各种排序丢进去之后太乱了。
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
char a[N];
int n,w,sa[N],rk[N],RK[N];
int MAX(int x,int y)
{
return x>y?x:y;
}
bool cmp(int x,int y)
{
if(rk[x]!=rk[y]) return rk[x]<rk[y];
return rk[x+w]<rk[y+w];
}
int main()
{
scanf("%s",a+1);
n=strlen(a+1);
int m=MAX(n,300);
for(int i=1;i<=n;i++) sa[i]=i,rk[i]=a[i];
//rk数组在代码中其实只关心大小关系,而并不关心具体的值
//进入循环后立刻就要排序,而且排序方法也和 sa 数组无关,所以 sa 数组的初值只要赋为 1~n 即可
for(w=1;w<n;w<<=1)
{
sort(sa+1,sa+n+1,cmp);
memcpy(RK,rk,sizeof(rk));
for(int t=0,i=1;i<=n;i++)
rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
}
for(int i=1;i<=n;i++) printf("%d ",sa[i]);
return 0;
}
应用
1
把字符串复制一遍,后缀排序即可。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
char a[N];
int n,sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int MAX(int x,int y)
{
return x>y?x:y;
}
int main()
{
scanf("%s",a+1);
n=strlen(a+1);
for(int i=1;i<=n;i++) a[i+n]=a[i];n*=2;
int m=MAX(n,300);
for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1)
{
int sum=0;
for(int i=n;i>n-w;i--) id[++sum]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w) id[++sum]=sa[i]-w;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
memcpy(RK,rk,sizeof(rk));
for(int t=0,i=1;i<=n;i++,m=t)
rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
}
for(int i=1;i<=n;i++)
if(sa[i]<=n/2) putchar(a[sa[i]+n/2-1]);
return 0;
}
2
先考虑暴力的做法。
首先,当首尾字符不同时,显然可以贪心选。
当首尾字符相同时,则把当前剩下的串和它的反串的字典序进行比较。
考虑如何优化这一做法。
把原串的反串接在原串后面,把两个串之间用一个字典序极小的字符隔开。
求出这个串的后缀数组,比较时直接比较 \(rk\) 即可。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e6+10;
char a[N],ans[N];
int n,sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int MAX(int x,int y)
{
return x>y?x:y;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) getchar(),a[i]=getchar();
a[n+1]='#';
for(int i=1,j=n;i<=n;i++,j--) a[n+1+i]=a[j];
n*=2,n++;
int m=MAX(n,300);
for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1)
{
int sum=0;
for(int i=n;i>n-w;i--) id[++sum]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w) id[++sum]=sa[i]-w;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
memcpy(RK,rk,sizeof(rk));
for(int t=0,i=1;i<=n;i++,m=t)
rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
}
n--,n/=2;
int l=1,r=n,res=0;
while(l<r)
{
if(a[l]<a[r]) ans[++res]=a[l],l++;
else if(a[r]<a[l]) ans[++res]=a[r],r--;
else
{
if(rk[l]<rk[n*2+2-r]) ans[++res]=a[l],l++;
else ans[++res]=a[r],r--;
}
}
ans[++res]=a[l];
for(int i=1;i<=res;i++)
{
putchar(ans[i]);
if(i%80==0) puts("");
}
return 0;
}
3
在线地在主串 \(T\) 中寻找模式串 \(S\) 。
发现若 \(S\) 在 \(T\) 中出现,\(S\) 一定是 \(T\) 某个后缀的前缀。
求出后缀数组,在求的过程中我们已经将后缀排序了,
在排序用的数组中二分,判断时暴力即可。
复杂度 \(O(|S| \log |T|)\) 。
若出现了很多次,发现每次出现时,我们要寻找的后缀在排序后一定是连续的,所以再二分一次即可。
\(height\) 数组
下面以 \(lcp(i,j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀的长度。
定义
\(height_i=lcp(sa_i,sa_{i-1})\) 。
求法
\(height_{rk_i} \geq height_{rk_{i-1}}-1\) 。
根据这个式子,按照 \(rk\) 的顺序暴力求,容易证明复杂度是 \(O(n)\) 的。
for(int i=1,t=0;i<=n;i++)
{
if(t) t--;
while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
ht[rk[i]]=t;
}
应用
1
求任意后缀的 \(lcp\) 。
\(lcp(x,y)=min\{height_k|rk_x <k \leq rk_y\}\) 。
2
求不同子串的数目。
子串就是后缀的前缀。
考虑容斥一下,用串的总数减去重复的串的个数。
按排序得到的顺序枚举后缀,发现每次重复的子串的数量即为在与前一个后缀的 \(lcp\) 里的前缀的数量。
所以,答案即为 \(\frac{n(n+1)}{2}-\sum \limits_{i=2}^n height_i\) 。
3
出现至少 \(k\) 次可以转化为在排序后的后缀中有至少连续 \(k\) 个后缀的 \(lcp\) 是这个串。
所以,只需求出每相邻 \(k-1\) 个 \(height\) 的最小值,在求出它们的最大值即可。
用单调队列实现。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<deque>
using namespace std;
const int N=2e6+10;
int sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int n,k,a[N],ht[N],ans;
struct node
{
int id,x;
};
deque <node> q;
int MAX(int x,int y)
{
return x>y?x:y;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int m=MAX(n,300);
for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1)
{
int sum=0;
for(int i=n;i>n-w;i--) id[++sum]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w) id[++sum]=sa[i]-w;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
memcpy(RK,rk,sizeof(rk));
for(int t=0,i=1;i<=n;i++,m=t)
rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
}
for(int i=1,t=0;i<=n;i++)
{
if(t) t--;
while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
ht[rk[i]]=t;
}
for(int i=2;i<=n;i++)
{
while(q.size()&&ht[i]<=q.back().x) q.pop_back();
q.push_back((node){i,ht[i]});
while(q.front().id<=i-k+1) q.pop_front();
if(i>=k) ans=MAX(ans,q.front().x);
}
printf("%d",ans);
return 0;
}
4
给出一个文本串,问是否有字符串在文本串中至少不重叠地出现了两次。
二分字符串的长度 \(x\) ,容易发现这一定是单调的,所以可以二分。
在 \(height\) 数组中找出所有连续 \(lcp\) 大于等于 \(x\) 的段,
对于每段找出后缀编号最小和最大的后缀,判断是否合法即可。
5
可以按照 \(height\) 数组大小的顺序合并答案,这部分用并查集维护。
因为最大的乘积可能是由最小值相乘,或由最大值相乘得到,所以要维护下最小值、最大值。
方案数即为合并时的两子树大小相乘。
发现若两个串是 \(r\) 相似的,则它们一定也是 \(1\) 相似,\(2\) 相似, \(\cdots\) ,\(r-1\) 相似的。
所以最后要做前缀和。
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define int long long
using namespace std;
const int N=2e6+10;
const int inf=1e18;
char a[N];
int sa[N],rk[N],RK[N],cnt[N],id[N],p[N],val[N];
int n,ht[N],fa[N],sum[N],ans[N],mx[N],mn[N],sz[N];
int MIN(int x,int y)
{
return x<y?x:y;
}
int MAX(int x,int y)
{
return x>y?x:y;
}
bool cmp(int x,int y)
{
return ht[x]>ht[y];
}
int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
int fx=find(x),fy=find(y);
sum[ht[x]]+=sz[fx]*sz[fy];
ans[ht[x]]=MAX(ans[ht[x]],MAX(mx[fx]*mx[fy],mn[fx]*mn[fy]));
mx[fx]=MAX(mx[fx],mx[fy]);
mn[fx]=MIN(mn[fx],mn[fy]);
fa[fy]=fx,sz[fx]+=sz[fy];
}
signed main()
{
scanf("%lld",&n);
scanf("%s",a+1);
for(int i=1;i<=n;i++) scanf("%lld",&val[i]);
int m=MAX(n,300);
for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1)
{
int sum=0;
for(int i=n;i>n-w;i--) id[++sum]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w) id[++sum]=sa[i]-w;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
memcpy(RK,rk,sizeof(rk));
for(int t=0,i=1;i<=n;i++,m=t)
rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
}
for(int i=1,t=0;i<=n;i++)
{
if(t) t--;
while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
ht[rk[i]]=t;
}
for(int i=1;i<=n;i++)
id[i]=i,fa[i]=i,ans[i]=-inf,sz[i]=1,mx[i]=val[sa[i]],mn[i]=val[sa[i]];
sort(id+1,id+n+1,cmp);
for(int i=1;i<=n;i++)
if(find(id[i])!=find(id[i]-1)) merge(id[i],id[i]-1);
for(int i=n-2;i>=0;i--)
sum[i]+=sum[i+1],ans[i]=MAX(ans[i],ans[i+1]);
for(int i=0;i<n;i++)
{
if(sum[i]==0) puts("0 0");
else printf("%lld %lld\n",sum[i],ans[i]);
}
return 0;
}
6
这个式子前面的部分可以直接算,所以就是在求后缀两两之间的 \(lcp\) 之和。
考虑 \(lcp\) 的求法,可以把求 \(lcp\) 之和转化为求 \(height\) 数组每段区间的区间最小值之和。
这就是单调栈经典问题了。
#include<iostream>
#include<cstring>
#include<cstdio>
#define int long long
using namespace std;
const int N=2e6+10;
const int inf=1e18;
char a[N],b[N];
int sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
int n,ht[N],ans,top,s[N],l[N],r[N];
int MAX(int x,int y)
{
return x>y?x:y;
}
signed main()
{
scanf("%s",a+1);
n=strlen(a+1);
int m=MAX(n,300);
for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
for(int w=1;w<n;w<<=1)
{
int sum=0;
for(int i=n;i>n-w;i--) id[++sum]=i;
for(int i=1;i<=n;i++)
if(sa[i]>w) id[++sum]=sa[i]-w;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
memcpy(RK,rk,sizeof(rk));
for(int t=0,i=1;i<=n;i++,m=t)
rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
}
for(int i=1,t=0;i<=n;i++)
{
if(t) t--;
while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
ht[rk[i]]=t;
}
ans=(n-1)*(n+1)*n/2;
ht[0]=-inf,ht[n+1]=-inf;
s[++top]=0;
for(int i=1;i<=n;i++)
{
while(top&&ht[i]<=ht[s[top]]) top--;
l[i]=s[top],s[++top]=i;
}
s[top=1]=n+1;
for(int i=n;i>=1;i--)
{
while(top&&ht[i]<ht[s[top]]) top--;
r[i]=s[top],s[++top]=i;
}
for(int i=1;i<=n;i++) ans-=2*(i-l[i])*(r[i]-i)*ht[i];
printf("%lld",ans);
return 0;
}