后缀数组一个优化是利用$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$数组性质即可.