1. 字尾陣列
1.1 內容
我們將一個字串 \(s\) 的所有字尾按照字典序從小到大排序得到陣列 \(sa\),其中 \(sa_i\) 表示以 \(sa_i\) 開始的字尾排名是第 \(i\) 個。
這個陣列就叫字尾陣列(Suffix Array, SA)。考慮到長度各不相同,所以顯然是個排列,設陣列 \(rk\) 是這個陣列的逆排列。
我們考慮倍增:先對所有長為 \(2^w\) 的子串排序,然後對於 \(2^{w+1}\) 的子串可以表示為一個二元組 \((rk_i, rk_{i+2^w})\),按照這個陣列排序就是 \(2^{w+1}\) 的子串的排序。
直接快排是 \(O(n \log^2 n)\),我們考慮最佳化掉快排,注意到值域不大,考慮基數排序。
我們先對第二關鍵字排序,再對第一關鍵字排序,由於桶排是穩定排序,這樣排就能排出來。
還有一個小最佳化,如果排序到某一步就各不相同了可以直接結束。
下面是模板題程式碼,參考了 qAlex_Weiq 的實現:
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6 + 5;
char s[N];
int n, sa[N] = {0}, rk[N] = {0}, buc[N] = {0}, ork[N] = {0}, id[N] = {0};
void SA() {
int m = (1 << 7), 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[rk[i]]--] = i;
for (int w = 1; ; m = p, p = 0, w <<= 1) {
for (int i = n - w + 1; i <= n; i++) id[++p] = i;
for (int i = 1; i <= n; i++) if (sa[i] > w) id[++p] = sa[i] - w;
p = 0;
for (int i = 1; i <= m; i++) buc[i] = 0;
for (int i = 1; i <= n; i++) buc[ork[i] = rk[i]]++;
for (int i = 1; i <= m; i++) buc[i] += buc[i - 1];
for (int i = n; i >= 1; i--) sa[buc[rk[id[i]]]--] = id[i];
for (int i = 1; i <= n; i++) rk[sa[i]] = (ork[sa[i - 1]] == ork[sa[i]] && ork[sa[i - 1] + w] == ork[sa[i] + w]) ? p : ++p;
if (p == n)
break;
}
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
SA();
for (int i = 1; i <= n; i++)
printf("%d ", sa[i]);
return 0;
}
1.2 height 陣列
SA 能解決一個很重要的問題:任意兩個子串的 LCP,而這就基於 height 陣列。
定義 \(height_i\) 表示 \(sa_{i-1}\) 與 \(sa_{i}\) 的 LCP 長度,再定義 \(h_i\) 表示 \(height_{rk_i}\)。我們有一條重要結論:
這就意味著我們可以類似 KMP 的方法來求出這個 \(height\) 陣列,時間複雜度 \(O(n)\)。
注意需要設定邊界字元。
s[0] = s[n + 1] = '#';
for (int i = 1, k = 0; i <= n; i++) {
if (k)
k--;
while (s[i + k] == s[sa[rk[i] - 1] + k])
k++;
ht[rk[i]] = k;
}
1.3 應用
求任意兩個子串的 LCP 長度
我們有一個很重要的結論:\(x,y\) 開始的字尾的 LCP 長度等於 \([rk_x, rk_y]\) 的區間中 \(height\) 的最小值,這個結論不難證明。
將子串轉化成字尾不影響,變成了 RMQ 問題,一般用 ST 表進行查詢。
P2408 不同子串個數
考慮到每個子串都是字首的字尾,所以我們可以列舉所有字尾。
我們考慮逐步計數,我們從 \(sa_1\) 開始計數,我們發現,\(sa_2\) 新增的子串個數剛好等於 \(|s[sa_2, n]| - height_{2}\),而後面的也是一樣的。
所以最終我們就得到答案為 \(\frac{n(n+1)}{2} - \sum_i height_i\)。
提交記錄
P5546 [POI2000] 公共串
首先對於多串問題,必須把他們都接在一起用特殊字元隔開,然後再跑 SA。
我們考慮二分,然後我們將所有 \(height < d\) 的斷開,分成若干段,則如果有一段的的字尾中包括了 \(n\) 種字串的就是可行的。
顯然我們可以用雙指標或者排序後並查集合並去掉二分。
提交記錄
P4248 [AHOI2013] 差異
將和式拆開,真正難算的是兩兩的 LCP 之和。
我們轉化成求所有區間的最小值之和,這個問題可以用單調棧來做。
我們定義最小值是最靠左的那個,這樣最小值就唯一了,然後處理出他作為最小值最遠的左右段點,然後直接計算總共有多少個區間即可,時間複雜度 \(O(n)\)。
提交記錄
P3181 [HAOI2016] 找相同字元
我們還是將兩個串接到一起求 SA。
然後我們發現答案實際上就是兩兩屬於不同字串的字尾的 LCP 之和,我們可以容斥,用總的減去兩個分別的,就是上面的問題。
提交記錄
CF123D String
第一種是將其轉化成兩兩的 LCP,但是這是因為這道題式子本身的特性。
還有一種可以對於任意的式子都能解決。
我們還是列舉最小值和所處區間,我們發現,這意味著這個最小值就去出現了這個區間的長度次,並且所有比這個區間左右兩邊小於最小值的還大的一直到最小值都是如此,我們可以直接統計這些子串的貢獻。
但是這樣就漏到了那些只出現一次的,所以我們還要用總的減去兩側 \(\height\) 的最大值,也就是那些不會被統計到的貢獻。
時間複雜度還是 \(O(n)\)。
提交記錄
P2178 [NOI2015] 品酒大會
我們考慮之前講過的哪種倒過來用並查集不斷合併統計貢獻的。
顯然兩個字尾的貢獻會在所有小於等於其 LCP 的時候被統計到,我們用並查集維護,合併時更新答案即可。
提交記錄