[字符串学习笔记] 6. 后缀数组

6.1. 约定

本文中,下标从 \(0\) 开始计算排名从 \(1\) 开始计算。当下标超出数组范围时,默认值为 \(0\)

将后缀 \(s[i \ldots |s| - 1]\) 称为后缀 \(i\)

将两个后缀 \(i, j\)最长公共前缀 长度简记为 \(f(i, j)\)

6.2. 定义

  • 定义 排名数组 \(\mathit{rk}[i]\)\(s[i \ldots |s| - 1]\)\(s\) 的所有后缀中按照 字典序 的排名。\(0 \leq i \leq |s| - 1\)\(\mathit{rk}\)\(1 \sim |s|\) 的排列,两两不同。
  • 定义 后缀数组 \(\mathit{sa}[i]\) 为排名第 \(i\) 的后缀在 \(s\) 中的起始位置。\(1 \leq i \leq |s|\)

不难发现,\(\forall i \in [0, |s| - 1] \cap \mathbb Z\),有 \(\mathit{sa}[\mathit{rk}[i]] = i\);而 \(\forall i \in [1, |s|] \cap \mathbb Z\),有 \(\mathit{rk}[\mathit{sa}[i]] = i\)

例如,当 \(s = \texttt{abaaab}\) 时,将所有后缀排好序:

\[\small \begin{array}{ccr} \text{rank} &\text{start pos} &\text{suffix} \\ 1 &2 &\texttt{aaab} \\ 2 &3 &\texttt{aab} \\ 3 &4 &\texttt{ab} \\ 4 &0 &\texttt{abaaab} \\ 5 &5 &\texttt{b} \\ 6 &1 &\texttt{baaab} \end{array} \]

即,

  • \(\mathit{sa} = [2, 3, 4, 0, 5, 1]\)(下标从 \(1\) 开始)。
  • \(\mathit{rk} = [4, 6, 1, 2, 3, 5]\)(下标从 \(0\) 开始)。

6.3. 实现

6.3.1. 朴素算法

存储所有后缀并进行 快速排序。比较字符串时间复杂度 \(\Theta(|s|)\),故总时间复杂度 \(\Theta({|s|}^2 \log |s|)\)

6.3.2. 倍增算法

如果要比较两个长度相等的字符串 \(s, t\),可以先将字符串各分成两部分 \((s_1, s_2), (t_1, t_2)\)\(|s_i| = |t_i|\)。将 \(s_1, t_1\) 的比较结果作为第一关键字,\(s_2, t_2\) 的比较结果作为第二关键字,就可以比较出 \(s, t\)

根据这种思路进行 倍增

首先,将 \(s\) 中的字符排序并给予排名 \(\mathit{rk}_1\)。注意此处 \(\mathit{rk}_1\) 不一定 两两不同。

对于 \(0 \leq i \leq |s| - 1\),将 \((\mathit{rk}_1[i], \mathit{rk}_1[i + 1])\) 二元组排序并给予排名 \(\mathit{rk}_2\)\(\mathit{rk}_2[i]\) 代表着 \(s[i \ldots \min(i + 1, |s| - 1)]\) 的排名。

对于 \(0 \leq i \leq |s| - 1\),将 \((\mathit{rk}_2[i], \mathit{rk}_2[i + 2])\) 二元组排序并给予排名 \(\mathit{rk}_4\)\(\mathit{rk}_4[i]\) 代表着 \(s[i \ldots \min(i + 3, |s| - 1)]\) 的排名。

同理,倍增地将 \((\mathit{rk}_w[i], \mathit{rk}_w[i + w])\) 二元组排序并给予排名 \(\mathit{rk}_{2w}\)\(\mathit{rk}_{2w}\) 代表着 \(s[i \ldots \min(i + 2w - 1, |s| - 1)]\) 的排名。

\(w \geq |s|\) 时,得到的 \(\mathit{rk}_w\) 即最终的 \(\mathit{rk}\) 数组。另外,若 \(\mathit{rk}_w\) 已然两两不同,再倍增下去显然 无法影响结果,可以直接退出倍增。

时间复杂度 \(\Theta(n \log^2 n)\)

代码
void SA(string s) {
    for (int i = 0; i < s.size(); i++)
        rk[i] = s[i], sa[i + 1] = i;
    auto R = [&](int *p, int ind) {
        return ind >= s.size() ? 0 : p[ind];
    }; // 防止越界,当然也可开双倍空间解决
    for (int w = 1; w < s.size(); w <<= 1) {
        sort(sa + 1, sa + s.size() + 1, [&](int i, int j) {
            return make_pair(R(rk, i), R(rk, i + w)) < make_pair(R(rk, j), R(rk, j + w));
        });
        copy(rk, rk + s.size(), ork);
        int top = 0;
        for (int i = 1; i <= s.size(); i++)
            if (i > 1 && R(ork, sa[i]) == R(ork, sa[i - 1]) && R(ork, sa[i] + w) == R(ork, sa[i - 1] + w))
                rk[sa[i]] = top;
            else
                rk[sa[i]] = ++top;
        if (top == n) break; // 互不相同,停止倍增
    }
}

6.3.3. 基数排序

由于双关键字排序的值域为 \(\Theta(n)\),故可以采用 基数排序 优化到 \(\Theta(n \log n)\)

小优化:在首先对第二关键字进行排序时,可以先将 \(\mathit{sa}[i] + w \geq n\)(超出数组范围即对应的 \(\mathit{rk}\) 值为 \(0\)最小)的 \(\mathit{sa}[i]\) 放到代码中 \(\mathit{id}\) 数组头部。

代码
void SA(string s) {
    int m = 128; // rk 的值域
    for (int i = 0; i < s.size(); i++)
        rk[i] = s[i], buc[rk[i]]++;
    for (int i = 1; i <= m; i++)
        buc[i] += buc[i - 1];
    for (int i = s.size() - 1; i >= 0; i--)
        sa[buc[rk[i]]--] = i;
    auto R = [&](int *p, int ind) {
        return ind >= s.size() ? 0 : p[ind];
    };
    for (int w = 1, top; w < s.size(); w <<= 1) {
        top = 0;
        for (int i = s.size() - w; i < s.size(); i++)
            id[++top] = i; // 顺序无关紧要
        for (int i = 1; i <= s.size(); i++)
            if (sa[i] >= w)
                id[++top] = sa[i] - w;
        fill(buc + 1, buc + m + 1, 0);
        for (int i = 0; i < s.size(); i++)
            buc[rk[i]]++;
        for (int i = 1; i <= m; i++)
            buc[i] += buc[i - 1];
        for (int i = s.size(); i >= 1; i--)
            sa[buc[rk[id[i]]]--] = id[i];
        copy(rk, rk + s.size(), ork);
        top = 0;
        for (int i = 1; i <= s.size(); i++)
            if (i > 1 && R(ork, sa[i]) == R(ork, sa[i - 1]) && R(ork, sa[i] + w) == R(ork, sa[i - 1] + w))
                rk[sa[i]] = top;
            else
                rk[sa[i]] = ++top;
        if (top == s.size()) break;
        m = top; // 更新值域
    }
}

6.4. Height 数组

6.4.2. 定义

定义 Height 数组 \(\mathit{ht}[i]\) 为排名第 \(i - 1\) 与第 \(i\) 的后缀的 最长公共前缀 长度,即 \(f(\mathit{sa}[i - 1], \mathit{sa}[i])\)

6.4.3. 实现

引理:\(\forall i \in [2, |s|] \cap \mathbb Z\)\(\mathit{ht}[\mathit{rk}[i]] \geq \mathit{ht}[\mathit{rk}[i - 1]] - 1\)

证明

  • \(\mathit{ht}[\mathit{rk}[i - 1]] \leq 1\) 时,\(\mathit{ht}[\mathit{rk}[i - 1]] - 1 \leq 0\),不等式显然成立。

  • \(\mathit{ht}[\mathit{rk}[i - 1]] > 1\) 时,代入得

    \[f(\mathit{sa}[\mathit{rk}[i - 1]], \mathit{sa}[\mathit{rk}[i - 1] - 1]) = f(i - 1, \mathit{sa}[\mathit{rk}[i - 1] - 1]) > 1 \]

    将这两个后缀的 最长公共前缀 表示为 \(c + S\),其中 \(c\) 为字符,\(S\) 为长度为 \(\mathit{ht}[\mathit{rk}[i - 1]] - 1\) 的字符串。此时,后缀 \(i - 1\) 可以表示为 \(c + S + U\),而后缀 \(\mathit{sa}[\mathit{rk}[i - 1] - 1]\) 可以表示为 \(c + S + V\),且有 \(U < V\)

    删去首字符 \(c\),得到后缀 \(i = S + U\),后缀 \(\mathit{sa}[\mathit{rk}[i - 1] - 1] + 1 = S + V\)。因为后缀 \(\mathit{sa}[\mathit{rk}[i] - 1]\) 仅次于 后缀 \(i\),又有 \(S + U < S + V\),则 \(S + U \leq\) 后缀 \(\mathit{sa}[\mathit{rk}[i] - 1] < S + V\)

    显然,\(S\) 为后缀 \(i\) 与后缀 \(\mathit{sa}[\mathit{rk}[i] - 1]\)公共前缀。则 \(f(i, \mathit{sa}[\mathit{rk}[i] - 1]) = \mathit{ht}[i] \geq |S| = \mathit{ht}[\mathit{rk}[i - 1]] - 1\)\(\square\)

根据上述引理,直接求即可。代码十分好写。

void SA_height(string s) {
    SA(s);
    for (int i = 0; i < s.size(); i++) {
        if (rk[i] == 1) continue;
        ht[rk[i]] = i == 0 ? 0 : max(0, ht[rk[i - 1]] - 1);
        while (i + ht[rk[i]] < s.size() && sa[rk[i] - 1] + ht[rk[i]] < s.size() && s[i + ht[rk[i]]] == s[sa[rk[i] - 1] + ht[rk[i]]]) // 需要判断是否越界
            ht[rk[i]]++;
    }
}

例题 SP32577 ADAPHOTO - Ada and Terramorphing

题解

引理:\(\forall i \in [3, |s|]\)\(f(\mathit{sa}[i], \mathit{sa}[j]) \leq \mathit{ht}[i] ~ (j < i - 1)\)

证明

若下标超出字符串范围,访问到的字符值为 \(-\infty\)

\(0\)\(\mathit{ht}[i] - 1\) 正序遍历 \(k\)。若 \(s[\mathit{sa}[j] + k] < s[\mathit{sa}[i] + k]\)\(f(\mathit{sa}[i], \mathit{sa}[j]) = k < \mathit{ht}[i]\)

如果 \(s[\mathit{sa}[j] \ldots \mathit{sa}[j] + \mathit{ht}[i] - 1] = s[\mathit{sa}[i] \ldots \mathit{sa}[i] + \mathit{ht}[i] - 1]\),则

\[s[\mathit{sa}[j] + \mathit{ht}[i]] \leq s[\mathit{sa}[i - 1] + \mathit{ht}[i]] < s[\mathit{sa}[i] + \mathit{ht}[i]] \]

\(f(\mathit{sa}[i], \mathit{sa}[j]) = \mathit{ht}[i]\)。综上,\(f(\mathit{sa}[i], \mathit{sa}[j]) \leq \mathit{ht}[i]\),得证。

接下来就不难做了。构造出字符串 \(p \gets s + \texttt \# + t\)\(\texttt \#\)\(s, t\) 字符集中未出现的 分隔符)。根据上述引理可知,最长公共子串一定是 \(p\) 排名相差 \(1\) 的后缀的最长公共前缀。如果 \(\mathit{sa}[i - 1], \mathit{sa}[i]\) 有一个小于 \(|s|\),而另一个大于 \(|s|\),说明这两个后缀一个位于 \(s\),一个位于 \(t\),符合题意,就统计答案。

时间复杂度瓶颈在于求 SA 的 \(\Theta(n \log n)\)。参考代码:

int res;
string s, t;

signed main() {
    cin >> s >> t;
    string p = s + '#' + t;
    SA_height(p);
    for (int i = 2; i <= p.size(); i++)
        if (sa[i - 1] < s.size() && sa[i] > s.size() || sa[i - 1] > s.size() && sa[i] < s.size())
            res = max(res, ht[i]);
    cout << res;
}

6.5. LCP 理论

6.5.1. LCP 引理

LCP 引理:

\[f(\mathit{sa}[i], \mathit{sa}[k]) = \min(f(\mathit{sa}[i], \mathit{sa}[j]), f(\mathit{sa}[j], \mathit{sa}[k])) ~ (1 \leq i \leq j \leq k \leq |s|) \]

证明

简记 \(l\)\(\min(f(\mathit{sa}[i], \mathit{sa}[j]), f(\mathit{sa}[j], \mathit{sa}[k]))\)。易知 \(f(\mathit{sa}[i], \mathit{sa}[j]), f(\mathit{sa}[j], \mathit{sa}[k])\)\(\geq l\)

因此,

\[{\color{#9f3} s[\mathit{sa}[i] \ldots \mathit{sa}[i] + l - 1]} = s[\mathit{sa}[j] \ldots \mathit{sa}[j] + l - 1] = {\color{#9f3} s[\mathit{sa}[k] \ldots \mathit{sa}[k] + l - 1]} \]

可得 \(f(\mathit{sa}[i], \mathit{sa}[k]) \geq l\)。不妨假设 \(f(\mathit{sa}[i], \mathit{sa}[k]) \geq l + 1\)

考虑 \(l\) 的定义,必然满足

  • \(s[\mathit{sa}[i] + l] < s[\mathit{sa}[j] + l]\)
  • \(s[\mathit{sa}[j] + l] < s[\mathit{sa}[k] + l]\)

二者中 至少一项。无论如何,\(s[\mathit{sa}[i] + l] < s[\mathit{sa[k]} + l]\),与假设相矛盾。故 \(f(\mathit{sa}[i], \mathit{sa}[k]) = l\)\(\square\)

6.5.2. LCP 定理

LCP 定理:

\[f(\mathit{sa}[i], \mathit{sa}[j]) = \min_{i + 1 \leq k \leq j} \mathit{ht}[k] \]

证明

结合 LCP 引理易证。

\[\begin{aligned} f(\mathit{sa}[i], \mathit{sa}[j]) &= \min(f(\mathit{sa}[i], \mathit{sa}[i + 1]), f(\mathit{sa}[i + 1], \mathit{sa}[j])) \\ &= \min(f(\mathit{sa}[i], \mathit{sa}[i + 1]), f(\mathit{sa}[i + 1], \mathit{sa}[i + 2]), f(\mathit{sa}[i + 2], \mathit{sa}[j])) \\ &= \ldots \\ &= \min_{i - 1 \leq k \leq j} f(\mathit{sa}[k - 1], \mathit{sa}[k]) \\ &= \min_{i - 1 \leq k \leq j} \mathit{ht}[k] \end{aligned} \]

posted @ 2024-07-06 12:41  Carrot-Meow~  阅读(39)  评论(0)    收藏  举报