后缀数组一个优化是利用$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) {
a[n+1] = rk[n+1] = h[n+1] = 0;
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$数组性质即可.
练习3. SPOJ - DISUBSTR Distinct Substrings
大意: 求本质不同子串数.
答案等于$n(n+1)/2-\sum h$, 因为每个后缀贡献是所有前缀减去重复计算的部分, 重复部分即为$h$
练习4. poj 2406
大意: 给定串, 求最多划分为多少个相同的子串.
求循环节等价于求前后缀匹配的最大长度, 显然直接kmp即可. 用$SA$的话可以直接暴力枚举每个后缀与整个串的$lcp$取最大即可. 这题数据很大, 倍增好像过不去, 需要用DC3或者SA-IS
练习5. SPOJ Repeats
大意: 求字典序最小的重复次数最多的连续重复子串.
相当于还是求循环节. 可以得到对于后缀$i,j$的贡献为$1+\lfloor\frac{lcp}{abs(j-i)}\rfloor$
本来想着枚举$h$, 然后转化为求区间最小差, 但是似乎区间最小差是不可做的......
正确做法是枚举后缀间距$x$, 把整个串分成$\frac{n}{x}$块, 如果每块的答案可以$O(1)$计算, 那么总复杂度就为$O(nlogn)$.
对于每一块, 我们只计算起始位置的$lcp$值$ans$, 若$ans$恰好能整除$x$, 那么显然这一块内其他点的$lcp$值一定等于$<ans$. 若不能整除, 在上一块内找到一个能补齐的位置, 用该位置的$lcp$值更新答案即可.
练习6. poj 3693
大意: 练习5改为求字典序最小方案.
字典序感觉很难处理, 看网上题解都是直接暴力枚举$sa$验证所有可行长度. 我测了一下poj的数据能得到可行长度只有一种可能, 这数据也太弱了吧........
我随机数据测试了一下发现可行长度种数似乎是$O(\sqrt{n})$的? 比如$bbaabbabaabbaabbabaababaa$这个串可以有7种可行长度. 最坏复杂度也不知道能卡到多少, 就这样吧...
练习7. poj 3693
大意: 求最长公共子串
两个串接一下, 二分答案就行了, 就是看一个$h$数组的连通块中是否同时存在两种后缀.
练习8. poj 3415
大意: 给定串$A,B$, 求长度不少于$k$的公共子串对数.
对于$A,B$中两个后缀的贡献为$lcp-k+1$, 在每个$h\ge k$的连通块中算一下贡献就行了, 可以用单调栈来优化.
练习9. poj 3294
大意: 给定$n$个串, 求最长的出现次数$>n/2$的公共子串