后缀数组一个优化是利用$hash$, 直接$O(nlog^2n)$按照定义模拟
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define REP(i,a,n) for(int i=a;i<=n;++i)
#define PER(i,a,n) for(int i=n;i>=a;--i)
using namespace std;
typedef long long ll;
const int P = 1e9+7, INF = 0x3f3f3f3f;
const int N = 1e6+10;
char s[N];
int n, fac[N], f[N];
int Hash(int l, int r) {
int ret = (f[r]-(ll)f[l-1]*fac[r-l+1])%P;
if (ret<0) ret+=P;
return ret;
}
struct _ {
int l,r,id;
} a[N];
int cmp(_ a, _ b) {
int l=1,r=min(a.r-a.l+1,b.r-b.l+1),ret=0;
while (l<=r) {
int mid=(l+r)/2;
if (Hash(a.l,a.l+mid-1)==Hash(b.l,b.l+mid-1)) ret=mid,l=mid+1;
else r=mid-1;
}
return s[a.l+ret]<s[b.l+ret];
}
int main() {
fac[0]=1;
REP(i,1,N-1) fac[i]=fac[i-1]*991ll%P;
scanf("%s",s+1);
n = strlen(s+1);
REP(i,1,n) f[i] = (f[i-1]*991ll+s[i]-'a'+1)%P;
REP(i,1,n) a[i].l=i,a[i].r=n,a[i].id=i;
sort(a+1,a+1+n,cmp);
REP(i,1,n) printf("%d ",a[i].id);puts("");
}
然后是倍增做法, 每次暴力双关键字排序可以达到复杂度$O(nlog^2n)$.
其中${rk}_i$表示起始位置为$i$的后缀的排名, ${sa}_i$为排名为$i$的后缀的起始位置
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#define REP(i,a,n) for(int i=a;i<=n;++i)
#define PER(i,a,n) for(int i=n;i>=a;--i)
#define x first
#define y second
using namespace std;
typedef pair<int,int> pii;
const int N = 1e6+10;
char s[N];
int n, cnt, b[N], sa[N], rk[N];
pii f[N],g[N];
int main() {
scanf("%s", s+1);
n = strlen(s+1);
REP(i,1,n) b[i]=s[i];
sort(b+1,b+1+n),cnt=unique(b+1,b+1+n)-b-1;
REP(i,1,n) rk[i]=lower_bound(b+1,b+1+cnt,s[i])-b;
REP(k,0,30) {
REP(i,1,n) f[i].x=rk[i],f[i].y=(i+(1<<k))>n?0:rk[i+(1<<k)],g[i]=f[i];
sort(f+1,f+1+n),cnt=unique(f+1,f+1+n)-f-1;
REP(i,1,n) rk[i]=lower_bound(f+1,f+1+cnt,g[i])-f;
if (cnt==n) break;
}
REP(i,1,n) sa[rk[i]]=i;
REP(i,1,n) printf("%d ",sa[i]);puts("");
}
快排过程可以用基数排序来优化, 这样就可以得到$O(nlogn)$求出.
通过$sa$可以快速求出求任意两个后缀的$LCP$
记${suf}_i$表示以$i$开头的后缀, $LCP_{i,j}$为${suf}[{sa}_i]$和${suf}[{sa}_j]$的$LCP$的长度
一个结论是${LCP}_{i,j}=\min\limits_{i<k\le j} {LCP}_{k,k-1}$
定义高度数组$h_i={LCP}_{i,i-1}$, 然后用$RMQ$处理一下就可以$O(1)$得出答案
高度数组可以用$hash$按照定义$O(nlogn)$求出, 但是实际上有更好的方法
一个结论是$h[{rk}_i]\ge h[{rk}_{i-1}]-1$, 所以可以$O(n)$求出
$O(nlogn)$求$sa,rk,h$的完整代码如下
int c[N],rk[N],h[N],sa[N];
void build(int *a, int n, int m) {
int i,*x=rk,*y=h;
for(i=1;i<=m;i++) c[i]=0;
for(i=1;i<=n;i++) c[x[i]=a[i]]++;
for(i=1;i<=m;i++) c[i]+=c[i-1];
for(i=n;i;i--) sa[c[x[i]]--]=i;
for(int k=1,p;k<=n;k<<=1) {
p=0;
for(i=n-k+1;i<=n;i++) y[++p]=i;
for(i=1;i<=n;i++) if(sa[i]>k) y[++p]=sa[i]-k;
for(i=1;i<=m;i++) c[i]=0;
for(i=1;i<=n;i++) c[x[y[i]]]++;
for(i=1;i<=m;i++) c[i]+=c[i-1];
for(i=n;i;i--) sa[c[x[y[i]]]--]=y[i];
swap(x,y); x[sa[1]]=1; p=1;
for(i=2;i<=n;i++)
x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p:++p;
if(p==n) break; m=p;
}
for(i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1,j,k=0;i<=n;i++) {
if(k) k--;
j=sa[rk[i]-1];
while(a[i+k]==a[j+k]) k++;
h[rk[i]] = k;
}
}
练习1. poj1743
大意: 给定串, 求最长的两个不相交子串, 满足子串相邻位置差值相同. 若答案<5输出0.
差分后就转为求相同的最长不相交子串, 考虑二分答案. 那么在$sa$数组中, 一个$h$值全部$\ge x$的连通块中, 任意两串$lcp$值均$\ge x$, 判断位置差的最大值是否$\ge x$即可.
练习2. poj 3261
大意: 求重复次数至少为$k$的子串最大长度.
二分答案+hash做法很显然. 后缀数组做法跟练习1完全一样, 利用$h$数组性质即可.