后缀数组(SA)学习笔记

鲜花:我不信这辈子我学不懂 SA。


定义

$sa(i)$ 代表字符串 $s$ 字典序排名为 $i$ 的后缀的起始位置。

$rk(i)$ 起始位置为 $i$ 的后缀在所有后缀中字典序的排名。

二者满足 $sa(rk(i)) = rk(sa(i)) = i$。

$height(i)$ 为排名为 $i$ 和 $i – 1$ 的后缀的最长公共前缀长度。

后缀排序

后缀排序需要用到倍增的思想。

假设我们知道了每个后缀只考虑前 $len$ 个字符的排名,那么可以推出只考虑前 $2len$ 个字符的排名,因此我们可以在 $O(\log n)$ 的时间完成后缀排序。

现可以使用 std::sort 实现 $O(n \log^2 n)$ 的后缀排序,具体地,在每次排序时对于后缀起始位置为 $i$ 的后缀,我们只需要建立一个二元组 $(rk_i, rk_{i + len})$,如果 $rk_{i} > n$ 则记 $rk_{i} = 0$,对这 $n$ 个二元组进行排序之后,后缀起始位置为 $i$ 的后缀构成的二元组在这 $n$ 个二元组中的排名便是新 $rk(i)$,注意如果两个二元组相同那么对应的 $rk$ 也应当相同。如果 $rk$ 是一个排列则说明后缀排序已经完成。

string s; cin >> s; 
int n = s.size(); s = " " + s;
vector<int> rk(n + 1, 1), sa(n + 1);
iota(sa.begin(), sa.end(), 0);
vector<AI> A(n + 1);
rep(i, 1, n) A[i][0] = s[i];

for(int len = 1, cnt = 0; ; len <<= 1, cnt = 0) {
    sort(sa.begin() + 1, sa.end(), [&](const int x, const int y) {
        return (A[x] != A[y] ? A[x] < A[y] : x < y);
    });
    rep(i, 1, n) {
        if(A[sa[i]] == A[sa[i - 1]]) rk[sa[i]] = rk[sa[i - 1]];
        else rk[sa[i]] = ++cnt;
    }
    if(cnt == n) break;
    rep(i, 1, n) {
        A[i][0] = rk[i];
        A[i][1] = (i + len <= n ? rk[i + len] : 0);
    }
}

rep(i, 1, n) cout << sa[i] << " \n"[i == n];

如果要优化成 $O(n \log n)$ 需要用到计数排序和基数排序,就是先对第二关键字进行桶排,再对第一关键字进行桶排。因为这个常数十分巨大,甚至比不过 $O(n \log^2 n)$,需要优化掉第二关键字排序,还需要调整桶的大小。

string s; 
int sa[N], rk[N], ork[N], buc[N], id[N], n;
void work() {
    int m = 1 << 7, p = 0;
    rep(i, 1, n) buc[rk[i] = s[i]]++;
    rep(i, 1, m) buc[i] += buc[i - 1];
    per(i, n, 1) sa[buc[rk[i]]--] = i;
    for(int w = 1 ; ; m = p, p = 0, w <<= 1) {
        rep(i, n - w + 1, n) id[++p] = i;
        rep(i ,1, n) if(sa[i] > w) id[++p] = sa[i] - w;
        memset(buc, 0, m + 1 << 2);
        memcpy(ork, rk, n + 1 << 2) ;
        p = 0;
        rep(i, 1, n) buc[rk[i]]++;
        rep(i, 1, m) buc[i] += buc[i - 1];
        per(i, n, 1) sa[buc[rk[id[i]]]--] = id[i];
        rep(i ,1, n) rk[sa[i]] = ork[sa[i - 1]] == ork[sa[i]] && ork[sa[i - 1] + w] == ork[sa[i] + w] ? p : ++p;
        if(p == n) return ;
    }
}

Height 数组

引理:$height(rk(i)) \ge height(rk(i – 1)) – 1$。

由这个引理可以写出来代码:

s += " ";
vector<int> ht(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++;    // 这里需要保证 s[0] 和 s[n + 1] 为空,多测的时候需要注意!
    ht[rk[i]] = k;
}

$height$ 能够用来求任意两个后缀的 $lcp$ 也就是 $lcp(i, j)$,简单地说,后缀 $i$ 和后缀 $j$,的后缀长度就是二者排名之间的 $height$ 数组最小值。

感性理解便是在 $rk(i) + 1$ 到 $rk(j)$ 中所有的后缀都需要满足 $lcp(i, j)$ 相同,相当于我们需要寻找到 $i – 1$ 和 $i$ 最小的 $lcp$ 也就是限制最严的一个,故维护区间最小值即可。(感觉和求 $lca(u, v)$ 的深度的 trick 有点像,不知道有没有本质关系。)

参考资料

对定义讲的非常浅显易懂:Demeanor_Roy 的专栏

对算法流程讲的非常浅显易懂:扶苏的博客

良好的代码实现和证明:Alex_Wei 的博客

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇