(sa) 和 (rk) 数组
定义
(sa_i) 表示将所有后缀按字典序排序后第 (i) 小的后缀的编号, (rk_i) 表示后缀 (i) 的排名。(后缀 (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
",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;
}