后缀数组
后缀排序
char s[N];
int n,sa[N],rk[N],ork[N<<1];
int buc[N],id[N],pid[N];
bool cmp(int a,int b,int w){return ork[a]==ork[b] && ork[a+w]==ork[b+w];}
void build()
{
int m=(1<<17),p=0;
for(int i=1; i<=n; i++) buc[rk[i]=s[i]]++;
for(int i=1; i<=m; i++) buc[i]+=buc[i-1];
for(int i=n; i>=1; i--) sa[buc[s[i]]--]=i;
for(int w=1; ; w<<=1,m=p,p=0)
{
for(int i=n; i>n-w; i--) id[++p]=i;
for(int i=1; i<=n; i++) if(sa[i]>w) id[++p]=sa[i]-w;
for(int i=0; i<=m+1; i++) buc[i]=0;
for(int i=1; i<=n; i++) buc[pid[i]=rk[id[i]]]++;
for(int i=1; i<=m; i++) buc[i]+=buc[i-1];
for(int i=n; i>=1; i--) sa[buc[pid[i]]--]=id[i];
for(int i=1; i<=n; i++) ork[i]=rk[i];
p=0;
for(int i=1; i<=n; i++) rk[sa[i]]=cmp(sa[i-1],sa[i],w)? p:++p;
if(p==n) break;
}
}
求 \(h\) 数组
关键性质:\(h_{rk_i}\ge h_{rk_{i-1}}-1\)
int k=0;
for(int i=1; i<=tot; i++)
{
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
h[rk[i]]=k;
}
应用
1、求任意两个后缀的 LCP
2、求本质不同的子串个数
3、与单调栈结合
将 \(h\) 数组倒过来可以得到一个柱状图,这是用单调栈解决问题的基础。以 P4248 [AHOI2013] 差异 为例,求两两后缀 \(\rm lcp\) 之和。
考虑按 \(rk\) 从小到大加入 \(h\)(其实就是依次加入 \(h_{1\sim n}\)),设 \(F(i)=\sum\limits_{p=1}^{i-1}|\operatorname{lcp}(sa_i,sa_p)| =\sum\limits_{p=1}^{i-1}\min\limits_{q=p+1}^i h_q\),答案即 \(\sum F(i)\)。用单调栈拆掉那个 \(\min\),变成维护单调栈矩形面积和。这是我们熟悉的问题。
一些题目
CF822E Liar
考虑朴素 DP,设 \(f_{i,j}\) 表示匹配到 \(s\) 的第 \(i\) 位,\(t\) 的第 \(j\) 位的最小次数,转移即考虑 \(s[i+1:]\) 和 \(t[j+1:]\) 的最长公共前缀。考虑优化,注意到 \(x\leq 30\),所以值域定义域互换,设 \(f_{i,a}\) 表示用了 \(s\) 前 \(i\) 位,\(a\) 个子串,最远能匹配到 \(t\) 的哪一位。转移不表。
P2178 [NOI2015] 品酒大会
因为是 \(r\) 相似也就一定是 \(0\sim r-1\) 相似,所以从大到小考虑。与 lcp 有关的是夹在这两个后缀排名间 \(h_i\) 的 \(\min\),考虑并查集,每次将 \(h_i=r\) 的后缀 \(sa_i\) 和 \(sa_{i-1}\) 合并,那么只需维护联通块大小,最大值、次大值、最小值、次小值即可。
P5341 [TJOI2019] 甲苯先生和大中锋的字符串
使用 SA 分析字符串的常见方式:将字符串按照 \(rk\) 排序。
我们在排序后的字符串取出一个长度为 \(k\) 的区间 \([i,i+k-1]\),显然这个区间的长度上界为 \(\operatorname{lcp}\limits_{j=i}^{i+k-1}s_j\)。下界就是 \(\max(h_{i+k},h_{i})\)。单调队列即可 \(\mathcal{O}(n)\)。