后缀数组
参考:https://www.bilibili.com/video/av92589768?from=search&seid=11036159274843024348
符号
子串
从原串中选取连续的一段即为子串,空串也是子串
后缀
我们用(suf(k))表示(s(k…n))构成的子串
任何子串都是某个后缀的前缀
最长公共前缀lcp
(lcp(suf(i), suf(j))) 表示两个串(suf(i))和(suf(j))最长的一样的前缀
问题
将所有后缀(suf(1),suf(2),…,suf(N))按照字典序从小到大排序
方法1
首先看到题目想到的就是直接用暴力,建一个(cmp)数组,用(string)可以比较大小的性质去暴力(sort)
因为(sort)是(nlog n)的,每次(cmp)函数都是(O(n))的,所以总的时间复杂度就是 (n^2log n)
方法2
想一想更好的做法,我们可以用二分+hash
复杂度:(n log^2n)
(cmp)函数中二分(suf(i))和(suf(j))的(lcp)
(return s[i + |lcp|] < s[j +|lcp|])
方法3
(SA)算法
$SA[l] = $ 排名第(l)的后缀的开始位置
$Rank[i] = $ 后缀(suf(i))的排名
Rank[SA[l]] = l;
SA[Rank[i]] = i;
求出其中一个就能(O(n))求出另一个
有什么求其中一个数组的好的方法呢?
答案是倍增
方法三实现优化
倍增
记(sub[i][k] = s)从(i)开始长度(=s^k)的子串
(sub[i][k]=s[i…i+(1 << k) - 1]),超过(n)的部分都视为' '(字典序最小的字符)
(rank[i][k] = sub[i][k])在长度(=2^k)的所有子串中的排名
$sa[l][k] = (在长度)=2^k(的所有子串中排名第)l$的子串的开始位置
过程
- 求出(sub[1][0], sub[2][0], …,sub[n][0])的字典排序
- 求出(sub[1][1], sub[2][1], …,sub[n][1])的字典排序
- ……
- 求出(sub[1][k], sub[2][k],…,sub[n][k])的字典排序
当子串长度(2^k>=n)时,子串排序就是后缀排序
利用(rank[1…n][k]),如何求出(rank[1…n][k+1])
对于两个子串(sub[i][k+1])和(sub[j][k+1])
先比较(rank[i][k]<rank[j][k])
若相等,再比较(rank[i+2^k][k]<rank[j+2^k][k])
其实就相当于对二元组((rank[i][k], rank[i+2^k][k]))排序
(pair)排序时,先按(first)比较,若相等再按(second)比较
但如果建(pair)数组直接(sort)的话,复杂度还是(nlog^2n),还不如写二分+hash
于是这个时候就出现了一个神奇的东西:基数排序
为什么可以优化呢?我们注意到(rank)这个数组,他的值域是多少?
没错,值域就是不超过(n)的正整数,所以我们就可以用基数排序,换句话说就是桶排序
关于基数排序的相关,看可以去看一下洛谷日报第十五期,这里给出链接:基数排序
写(SA)时的基数排序用(cnt)实现
如何将(a[i])数组基数排序,然后将结果放在(SA)数组中呢?
下面的代码就实现了输入一个(a)数组,得到(sa)数组
for (int i = 1; i <= n; i++) ++cnt[a[i]];
for (int i = 1; i <= n; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[a[i]]--] = i;
比如一个(a)数组为
(a=[2,1,2,4,2])
若用(sa[l])表示排名第(l)的数在(a)中的下标
则(sa=[2,1,3,5,4])
就可以根据
Rank[SA[l]] = l;
SA[Rank[i]] = i;
得出(rank)数组
(rank=[2,1,2,3,2])
到这里我们就能回到一开始的问题,实现用(rank[1…n][k]),如何求出(rank[1…n][k+1]),步骤如下:
(large for(k = 1 sim log n))
- 按(rank[i+2^k][k])(第二关键字)基数排序
- 按(rank[i][k])(第一关键字)基数排序,得到(sa[i][k+1])数组
- 由(sa[i][k+1])求出(rank[i][k+1])
如果你细心的话可能会发现,(k)是从(1)开始的而不是从(0)开始的,那么(k)是(0)时候怎么来的呢?
因为(2^0)就是(1),所以我们可以直接把(rank)数组(也就是排名)先设成当前字符的( ext{ASCII})码,这样就可以啦~
sa->rank
如果(rk[i])中有并列
for (int p = 0, i = 1; i <= n; i++) {
if(oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + k] == oldrk[sa[i - 1] + k])
rk[sa[i]] = p;
else rk[sa[i]] = ++p;
}
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int A = 1e6 + 11;
inline int read() {
char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}
char s[A];
int n, m, sa[A], rank[A], tp[A], tax[A];
void cntsort() {
for (int i = 0; i <= m; i++) tax[i] = 0;
for (int i = 1; i <= n; i++) tax[rank[i]]++;
for (int i = 1; i <= m; i++) tax[i] += tax[i - 1];
for (int i = n; i >= 1; i--) sa[tax[rank[tp[i]]]--] = tp[i];
}
void Sort() {
m = 75;
for (int i = 1; i <= n; i++) rank[i] = s[i] - '0' + 1, tp[i] = i;
cntsort();
for (int w = 1, p = 0; p < n; m = p, w <<= 1) {
p = 0;
for (int i = 1; i <= w; i++) tp[++p] = n - w + i;
for (int i = 1; i <= n; i++) if(sa[i] > w) tp[++p] = sa[i] - w;
cntsort();
swap(tp, rank);
rank[sa[1]] = p = 1;
for (int i = 2; i <= n; i++) {
rank[sa[i]] = (tp[sa[i - 1]] == tp[sa[i]] && tp[sa[i - 1] + w] == tp[sa[i] + w]) ? p : ++p;
}
}
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
Sort();
for(int i = 1; i <= n; i++) cout << sa[i] << ' ';
return 0;
}
Height数组
我们通过求(SA)数组可以把所有后缀排序,那么排序之后有啥用呢??
其实是为了快速的求出任意两个后缀的(lcp)长度
我们记$Height[l] = (排名第)l-1(的后缀和排名第)l(的后缀的)lcp$长度
(Height[l] = lcp(suf(SA[l-1], suf(SA[l]))))
(Height[1])可以视作(0)。
假设(l=)后缀(suf(i))的排名,$r = (后缀)suf(j)(的排名(在此)l(不一定小于)r$,只是举例)
那么有结论:
- (lcp(suf(i),duf(j))=min(Height[l+1]…Height[r]))
- 即两个后缀的(lcp=)它们排名区间中(Height)的最小值
可以用数据结构维护(rmp)
为什么可以这么理解呢?
假设有三个字符串(s_1,s_2,s_3),且(s_1<s_2<s_3)(按(rank)排名得出)
那么(lcp(s_1,s_3))就等于(min(lcp(s_1,s_2), lcp(s_2,s_3)))
(详细证明需要画图……我真的懒)
(lcp(s_1,s_3) >= min(lcp(s_1,s_2), lcp(s_2,s_3))=1)
又有(s_1[l+1]!= s_3[l+1])
求法
那么如何快速求出(Height)数组呢?
纯暴力(O(n^2))
for i = 1 - N
l = rank[i]
j = sa[l - 1]
k = 0
while (s[i + k] ==s [j + k]): ++k
Height[l] = k
令(l = rank[i], r = rank[i-1])
(Height[l] = lcp(suf(SA[l-1]), suf(i)))
(Height[r] = 1cp(suf(SA[r-1]),suf(i-1)))
有重要结论:
(Height[l] >= Height[r] - 1)
- 若(Height[r]>1),有(suf(SA[r-1]) < suf(SA[i-1]))
- 去掉首个字符 (lcp(suf(SA[r-1]+1), suf(SA[i])) = Height[r] - 1)
- (suf(SA[r-1]+1) < suf(SA[i]))
- 由于$Height[1] (是)suf(i)(与排名紧挨着自己的后缀的)lcp$,有
- (suf(SA[r-1]+1) <= suf(SA[1-1]) < suf(SA[i]))
相近的(Height)会比较相似,比较远的会差别很大
不恰当的例子:
优化(O(n))
利用(Height[rank[i]] >= Height[rank[i-1] ] - 1)
优化暴力即可,复杂度(0(N))
for i = 1 - N
j = sa[l - 1]
k = max(0, Height[rank[i - 1]] - 1)
while (s[i + k] == S[j+k]): ++k
Height[rank[i]] = k
之后再用(st)表之类的维护(Height)的(rmq)信息即可