字串問題的終極武器不是 KMP,是 Suffix Array——一旦建好,幾乎所有字串查詢都能用二分搜尋解決。
為什麼需要它?
KMP 能找一個 pattern,但如果要做「最長重複子字串」「兩個字串的最長公共子字串」「字串裡有多少個不同子字串」,KMP 就不夠了。
後綴樹(Suffix Tree)可以處理,但實作複雜且常數大。Suffix Array 是後綴樹的「壓縮版」——用一個整數陣列存所有後綴的字典序排名,空間更省,搭配 LCP 陣列幾乎能解決所有後綴樹能解的問題。
核心結構
Suffix Array(SA):把字串所有後綴按字典序排序,記錄每個後綴的起始索引。
s = "banana"
後綴: 字典序排序:
0: banana → 5: a
1: anana 3: ana
2: nana 1: anana
3: ana 0: banana
4: na 4: na
5: a 2: nana
SA = [5, 3, 1, 0, 4, 2]
LCP 陣列:SA 中相鄰兩個後綴的最長公共前綴長度。
SA = [5, 3, 1, 0, 4, 2]
a ana anana banana na nana
LCP = [0, 1, 3, 0, 0, 2]
↑ ↑
ana/anana na/nana
共同前綴3 共同前綴2
建構:倍增法 O(n log² n)
每一輪把排名 key 的長度翻倍——先按 1 個字元排,再按 2 個字元,再按 4 個……log n 輪後全部後綴的相對排名確定。
round 0: 按 1 字元排 → [a, a, a, b, n, n]
round 1: 按前 2 字元排 → [a, an, an, ba, na, na](利用 round 0 排名)
...
LCP 陣列用 Kasai 演算法 O(n) 線性建構,利用「相鄰後綴的 LCP 至少是上一個 LCP - 1」的性質。
應用
// 模式搜尋 O(m log n)
// 在 SA 上二分:找到所有以 pattern 為前綴的後綴
int lo = lowerBound(sa, s, pattern);
int hi = upperBound(sa, s, pattern);
// SA[lo..hi] 的後綴都以 pattern 開頭,共 hi-lo+1 個出現位置| 問題 | 解法 | 複雜度 |
|---|---|---|
| 模式搜尋 | SA 上二分 | O(m log n) |
| 最長重複子字串 | LCP 陣列最大值 | O(n) |
| 不同子字串數量 | n(n+1)/2 − sum(LCP) | O(n) |
| 兩字串最長公共子字串 | 合併後建 SA,找跨分隔符的最大 LCP | O(n log n) |
複雜度
| 時間 | 空間 | |
|---|---|---|
| 建 SA(倍增) | O(n log² n) | O(n) |
| 建 SA(SA-IS) | O(n) | O(n) |
| 建 LCP(Kasai) | O(n) | O(n) |
| 模式搜尋 | O(m log n) | — |
SA-IS 是線性建構演算法,實作複雜但競賽常見。面試和一般工程用倍增法就夠。
Suffix Array 是字串世界的「預處理換查詢」——建一次,幾乎所有字串查詢都能 O(log n) 或 O(1) 回答。