线性求后缀数组

虽然从 DC3 到 SA-IS 的线性构造后缀数组的算法现在应该都是老生常谈了,但是为了<del>先把下篇鸽掉</del>对后缀数组算法有更详尽的了解,啊窝编不下去了,还是进入正题吧。

其实之前也早就有博客来说这种东西了,比如 诱导排序与SA-IS算法 - riteme.site ,不过窝还是比较喜欢一些短篇幅的简明介绍(摊手



后缀数组

后缀数组即将一个字符串 s 的后缀排序后的得到的数组,由于其本身和后缀树的 dfs 序有一定联系,也有很多很好的性质,在其上建立了很多的算法。

在OI里最常见(咸鱼)之一的算法应该是倍增算法,从长度为 2^k 的子串的 SA 来推断长度为 2^{k+1} 的子串的 SA ,只需要将 S_{k+1}[i] 表示成一个二元组 (\mathrm{rank}_k[i],\mathrm{rank}_k[i+2^k]) 即可用基数排序求得结果。


DC3

比较两个后缀 s[i:]s[j:] 可以先比较第一个字符,然后(如果相等的话)就转化为比较 s[i+1:]s[j+1:]

这启发我们可以将所有 i\bmod 3=0i\bmod 3=1 的后缀排序,之后对任意两个后缀的比较都可以在常数时间内转化到比较这两种后缀的比较上,而这两种后缀只占所有后缀的 2/3 ,可以递归处理。


算法轮廓:

  1. 基数排序并将每个字符转化为其 rank 。
  2. T_0=[(T[3i],T[3i+1],T[3i+2])\ i=0,1,\ldots] \\T_1=[(T[3i+1],T[3i+2],T[3i+3])\ i=0,1,\ldots]\\T_2=[(T[3i+2],T[3i+3],T[3i+4])\ i=0,1,\ldots] (三元组的序列)
  3. 递归将 [T_0,T_1]T_0 后面接 T_1 )排序
  4. 基数排序 T_2[i:]=(T[3i+2],T_0[i+1])
  5. 合并 SA_0,SA_1,SA_2 得到 SA 。


时间复杂度 T(n)=T(\frac{2}{3}n)+O(n)=O(n)


SA-IS

我们继续之前的思考,对两个后缀的比较可以先比较第一段连续相同的字符,比如说 bbbb\ldots 就可以表示成 (b,4) ,相同的话再继续比较,而如果不同的话,就要看下一段是不是比这一段大,比如说我们定义 bbcc 为 S 型, bbaa 为 L 型,那么显然 (b,1,type L)<(b,2,typeL)<\ldots (b,\infty,typeL)<(b,\infty,typeS)<\ldots <(b,1,typeS)

这就很容易引出我们对正式的 S,L 型的定义:若 s[i:]<s[i+1:]s[i]s[i:] 为 S 型,否则为 L 型。

实际上来说 S 型就是 s[i]<s[i+1] 或者, s[i]=s[i+1]s[i+1] 为 S型。L型类似。


我们如果知道了每个 (x,1,typeL) 的 rank (x是任意字符),那我们可以这样排序所有 L 型后缀:

void inductL(int *sa) {
    pos[1] = 1;
    for (int i = 1; i < n; ++i) pos[i + 1] = pos[i] + cnt[i];
    for (int i = 1; i <= n; ++i)
        if (sa[i] != -1 && sa[i] != 1 && t[sa[i] - 1])
            sa[pos[s[sa[i] - 1]]++] = sa[i] - 1;
}

这实际上是由于我们之前所说明的大小关系,所有以 b 开头的后缀的大小关系一定是 (b,1,type L)<(b,2,typeL)<\ldots (b,\infty,typeL)<(b,\infty,typeS)<\ldots <(b,1,typeS) ,所以我们一定是先扫描所有 (b,1,type L) ,再扫描 (b,2,typeL) ,以此类推。

而 S 型后缀也是同理,只要从后往前做即可。

于是我们可以把每个 S...SL...L 这样的子串(记为 LMS 子串)单独考虑,这样的话我们就只需要把所有LMS子串的第一个 S 的后缀排序即可(记为 SA_1),然后我们就可以用上面的方法推导出所有后缀的大小关系,具体来说就是(第i个子串的起始位置记为 p_i ):

void sortLMS(int *sa, int n, int N) {
    for (int i = 1; i <= n; ++i) pos[i] = pos[i - 1] + cnt[i];
    for (int i = N; i >= 1; --i) {
        int k = p[sa1[i]];
        sa[pos[s[k]]--] = k;
    }
    inductL(sa);
    inductS(sa);
}

注意到这里有一个细节要注意,由于 s[p_i:] 是 S 型后缀,所以在把 SA1 放进 SA 的时候要从后往前,这样放到 S 型后缀的位置里,不要影响 inductL , inductR 的时候将其覆盖即可。

而这样的话我们可以证明,我们可以在把所有 LMS 子串排序后把它们都缩成它们的 rank (这是比较容易证明的),再对新串排序,注意到新串的长度至多是原串一半,所以我们可以递归下去处理。

至于对 LMS 子串排序,注意到其实这个过程和我们推导所有后缀的大小关系过程是相同的,只不过 SA_1 是任意的(由于 LMS 子串后面是空的)。


感觉写的有点乱,代码也没有更,等回到家<del>懒癌治好</del>的时候再继续吧。

同时欢迎像 whx 一样的投稿~


Upd 2018.05.01

无意间翻出了底下代码的(正确?)出处:sunmoon-template.blogspot.sg


Upd 2017.8.14.

看到这个代码以后我实在是写不出比这个更简单明了的代码了(来源:Universal Online Judge):

#define pushS(x) sa[cur[s[x]]--] = x
#define pushL(x) sa[cur[s[x]]++] = x
#define inducedSort(v) fill_n(sa, n, -1); fill_n(cnt, m, 0);                  \
    for (int i = 0; i < n; i++) cnt[s[i]]++;                                  \
    for (int i = 1; i < m; i++) cnt[i] += cnt[i-1];                           \
    for (int i = 0; i < m; i++) cur[i] = cnt[i]-1;                            \
    for (int i = n1-1; ~i; i--) pushS(v[i]);                                  \
    for (int i = 1; i < m; i++) cur[i] = cnt[i-1];                            \
    for (int i = 0; i < n; i++) if (sa[i] > 0 &&  t[sa[i]-1]) pushL(sa[i]-1); \
    for (int i = 0; i < m; i++) cur[i] = cnt[i]-1;                            \
    for (int i = n-1;  ~i; i--) if (sa[i] > 0 && !t[sa[i]-1]) pushS(sa[i]-1)

void sais(int n, int m, int *s, int *t, int *p) {
    int n1 = t[n-1] = 0, ch = rk[0] = -1, *s1 = s+n;
    for (int i = n-2; ~i; i--) t[i] = s[i] == s[i+1] ? t[i+1] : s[i] > s[i+1];
    for (int i = 1; i < n; i++) rk[i] = t[i-1] && !t[i] ? (p[n1] = i, n1++) : -1;
    inducedSort(p);
    for (int i = 0, x, y; i < n; i++) if (~(x = rk[sa[i]])) {
        if (ch < 1 || p[x+1] - p[x] != p[y+1] - p[y]) ch++;
        else for (int j = p[x], k = p[y]; j <= p[x+1]; j++, k++)
            if ((s[j]<<1|t[j]) != (s[k]<<1|t[k])) {ch++; break;}
        s1[y = x] = ch;
    }
    if (ch+1 < n1) sais(n1, ch+1, s1, t+n, p+n1);
    else for (int i = 0; i < n1; i++) sa[s1[i]] = i;
    for (int i = 0; i < n1; i++) s1[i] = p[sa[i]];
    inducedSort(s1);
}


至于 DC3 和 SAM 谁更快,我发现虽然 SAM 的实现看起来比较固定,但是我不知道 DC3 怎么实现比较快啊……等我找到好的 DC3 实现再来更。

文章被以下专栏收录