后缀数组
先下几个常见的定义
(s(i, j))表示([i, j])形成的连续子串
(suf[i])表示以(i)为开头的后缀
(rank)数组:(rank[i])表示将(1sim n)的后缀排序后,(suf[i])的排名
(sa)数组:(sa[i])表示将(1 sim n)的后缀排序后,排第(i)的在哪里
举个例子:
sort(a + 1, a + n + 1, cmp);
当我们采用上述代码之后,(a)数组存下的实际就是排第(i)的在哪里
(sa)数组不会相同,因为后缀的长度互不相同
在假想状态下,我们考虑在字符串的某尾加上无限个(0)
这样子,我们可以使得两个后缀具有相同的长度,便于比较
比如字符串"(abaa)"
我们实际上是在比较这4个字符串的排名:
(abaa,baa0,aa00,a000)
先看一个有趣的事情:
对于字符串(S = S_1 + S_2, T = T_1 + T_2),且(|S_1| = |S_2|, |T_1| = |T_2|)
如果(S_1 < T_1),那么(S < T)
如果(S_1 = T_1;and;S_2 < T_2),那么(S < T)
这要怎么利用呢?
也就是说,我们现在把所有的后缀都看做是长度为(n)的字符串
我们先处理出所有的(s(i, i))的字典序排名,如果不存在(s(i, i) = s(j, j)),那么我们的序排好了
否则,我们可以利用所有的(s(i, i)),按照上面的排序方式,得出所有的(s(i, i + 2^1 - 1))的字典序排名
同样,如果不存在(s(i, i + 2^1 - 1) = s(j, j + 2^1 - 1)),那么我们的序就排好了
否则,合并出(s(i, i + 2^2 - 1)),然后再去判断
依次类推
当我们合并到(s(i, i + 2^k - 1);(2^k geq n))时,我们一定能判断出字典序排名
如果合并的时候,我们的复杂度可以做到(O(n log n)),那么总体而言,就能做到(O(n log ^2 n))
如果合并的时候,我们能做到(O(n)),那么总体而言,就能做到(O(n log n))
可以对着上面这张经典的图理解一下
在描述(O(n log n))的鬼畜写法之前,我们先来看看(O(n log^2 n))的写法
inline bool cmp(int x, int y) { return P[x] < P[y]; }
inline void Suffix_sort() {
for(int i = 1; i <= n; i ++) sa[i] = i;
for(int i = 1; i <= n; i ++) rk[i] = s[i];
//初始化sa和rank
for(int k = 1; k <= n; k <<= 1) {
//倍增
for(int i = 1; i <= n; i ++)
P[i] = make_pair(rk[i], rk[i + k]);
//通过stl的pair+sort来实现双关键字排序
sort(sa + 1, sa + n + 1, cmp);
int tmp = 1; rk[sa[1]] = 1;
for(int i = 2; i <= n; i ++)
rk[sa[i]] = (P[sa[i]] == P[sa[i - 1]]) ? tmp : ++ tmp;
//排完序之后,按照字典序的顺序对每个点重新计算rank
if(tmp >= n) break;
//如果当前的排名已经 >= n,代表不存在两个相同的量,也就是排完了
}
}
(它在luogu上跑过了1000000)
请确保你在明白了(O(n log^2 n))的写法后,再继续往下阅读
我们只需要把上面的(sort)换成基数排序即可
基数排序原理十分的好懂,请自行了解
void sort(int *a, int n, int m) {
for(int i = 1; i <= n; i ++) cnt[p2[i]] ++;
for(int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for(int i = 1; i <= n; i ++) b[cnt[p2[i]] --] = i;
//b用来暂时存储对第二关键字排完序之后的结果
for(int i = 0; i <= m; i ++) cnt[i] = 0;
for(int i = 1; i <= n; i ++) cnt[p1[i]] ++;
for(int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i --) a[cnt[p1[b[i]]] --] = b[i];
//这里一定要倒叙枚举
for(int i = 0; i <= m; i ++) cnt[i] = 0;
}
inline void Suffix_sort() {
for(int i = 1; i <= n; i ++) sa[i] = i;
for(int i = 1; i <= n; i ++) rk[i] = s[i];
int m = 128; //m初始化为字符集的大小
for(int k = 1; k <= n; k <<= 1) {
for(int i = 1; i <= n; i ++)
p1[i] = rk[i], p2[i] = rk[i + k];
sort(sa, n, m);
int tmp = 1; rk[sa[1]] = 1;
for(int i = 2; i <= n; i ++)
rk[sa[i]] =
(p1[sa[i]] == p1[sa[i - 1]] && p2[sa[i]] == p2[sa[i - 1]]) ?
tmp : ++ tmp;
if(tmp >= n) break;
m = tmp;
}
}
那么我们能不能进一步优化呢?
当然是可以的,可以发现上面代码中的(rk, p1, p2)数组其实只需要保留两个即可
并且,对第二关键字实际上并不需要基排,只需要调用上一次的(sa)数组就可以得到结果
然而经过测试,发现在(10^6)的数据下只有(20ms)的常数差距
因此实际上没有必要学习网上流传的写法...
经过这么一段长长的文字,你终于懂了后缀数组,但是给后缀排序有啥用呢?
我们需要引入一个更强大的数组
(lcp(i, j))表示后缀(i)和后缀(j)的最长公共前缀
(height)数组,表示(lcp(suf[sa[i]], suf[sa[i - 1]])),即字典序第(i)小和字典序第(i - 1)小的最长公共前缀
一个十分重要的性质
对于后缀(suf[i])和(suf[j]),满足(lcp(i, j) = min(height[k]);(k in[rk[i] + 1, rk[j]]))
考虑证明:
首先证明,(lcp(i, j) leq min(height[k]);(k in[rk[i] + 1, rk[j]]))
我们记(min(height[k]) = h)
如果(lcp(i, j) > h),那么对于(k in [rk[i] + 1, rk[j]])而言
我们记(lcp(i, j) = L)
由于(k)的排名处于(i)和(j)之间,并且(i)和(j)的前(L)都相同,必然有(k)包含(L)这个前缀
否则,(k)的排名由于在(L)之前就出现了不同,因此要么在(i)之前,要么在(j)之后
因此,有(height[k] = L > h = min(height[k]))
这不可能,因此(lcp(i, j) leq h)
然后证明,可以取到上界
这是因为(height[i] geq h),可以看做(suf[sa[i]])和(suf[sa[i - 1]])至少有长为(h)的公共前缀
公共前缀满足传递性,因此可以取到上界
那么怎么求(height)数组呢?
如果暴力的求解,复杂度显然是(O(n^2))的
我们可以很清楚的知道(suf[sa[i]])和(suf[sa[i - 1]])在字符串构成的联系不如(suf[i])和(suf[i - 1])的联系
毕竟,(suf[i])和(suf[i - 1])只相差了一个字符
那么,我们直接按照下标顺序来计算的时候,可以发现
我们记(h[i])表示(suf[i])和字典序排在它前面的最长公共前缀
那么(h[i] geq h[i - 1] - 1)
比如现在正在求(h[i]),排在(i)之前的后缀是(suf[j])
后缀(i)为(abc...),后缀(j)为(aba...),那么(h[i] = 2)
求(h[i + 1])时,由于(h[i] geq 1;(h[i] = 0可以无视))
因此,去掉首字母的后缀(i + 1)为(bc...),后缀(j + 1)为(ba...)
可以发现,(j + 1)还是保持排在(i + 1)的前面,不妨设排在(i + 1)前面的后缀为(k)
那么,一定有(h[i + 1] = lcp(i + 1, k) geq lcp(i + 1, j + 1) = h[i] - 1)
和求后缀数组比起来,求(height)显得十分的简洁
void Solve() {
for(int i = 1, k = 0; i <= n; i ++) {
if(k) k --; //此时的k相当于h[i],得到的新k相当于h[i] - 1
int j = sa[rk[i] - 1];
while(s[j + k] == s[i + k]) k ++;
height[rk[i]] = k; // = h[i]
}
}
由于每次求(h[i])时,(k)指针都只会(- 1),也就是增加(1)的势能
而每次(k)往前挪移的时候,都需要(1)的势能
那么,势能总量是(O(n))的,也就是求(height)数组只需要(O(n))的复杂度