后缀数组倍增算法模板详解

参考

2009国家集训队论文 后缀数组——处理字符串的有力工具 ——罗穗骞

模板

bool cmp(int* r, int a, int b, int l) { return r[a] == r[b] && r[a+l] == r[b+l]; }

void init(int* r, int* sa, int n, int m) {
    int* x=wa, *y=wb, *t, i, j, p;
    for (i = 0; i < m; ++i) wt[i] = 0;
    for (i = 0; i < n; ++i) ++wt[x[i] = r[i]];
    for (i = 1; i < m; ++i) wt[i] += wt[i - 1];
    for (i = n-1; i >= 0; --i) sa[--wt[x[i]]] = i;

    for (j = 1, p = 1; p < n; j <<= 1, m = p) {
        //注意!每次循环要记得更新m的值!
        for (p = 0, i = n-j; i < n; ++i) y[p++] = i;
        for (i = 0; i < n; ++i) if (sa[i] >= j) y[p++] = sa[i] - j;

        for (i = 0; i < n; ++i) wv[i] = x[y[i]];

        for (i = 0; i < m; ++i) wt[i] = 0;
        for (i = 0; i < n; ++i) ++wt[wv[i]];
        for (i = 1; i < m; ++i) wt[i] += wt[i - 1];
        for (i = n-1; i >= 0; --i) sa[--wt[wv[i]]] = y[i];

        t = x, x = y, y = t, x[sa[0]] = 0;
        for (p = 1, i = 1; i < n; ++i) x[sa[i]] = cmp(y, sa[i], sa[i-1], j) ? p - 1 : p++;
    }

    for (i = 0; i < n; ++i) rk[sa[i]] = i;
    int k = 0;
    for (i = 0; i < n - 1; h[rk[i++]] = k) {
        //注意!这里上界是n-1而不是n,因为sa[n-1] = 0
        for (k = k ? --k : 0, j = sa[rk[i] - 1]; r[i+k] == r[j+k]; ++k);
    }
}

解释

请在理解了算法的基础上食用

参数

void init(int* r, int* sa, int n, int m);

r :原字符串

sa:后缀数组, sa[i]=j 的含义是 rank[suffix(j)]=i ,即后缀 [j,n1] 的排名是 i ,亦即排名第 i 的字符串的开头位置是 j

n:原字符串的长度,保证 r[n1]=0

m :原字符串中最大的字符值

第一部分

    for (i = 0; i < m; ++i) wt[i] = 0;
    for (i = 0; i < n; ++i) ++wt[x[i] = r[i]];
    for (i = 1; i < m; ++i) wt[i] += wt[i - 1];
    for (i = n-1; i >= 0; --i) sa[--wt[x[i]]] = i;

wt[ ] 数组完成计数排序的功能。

x[ ] 数组和后面的用法统一, x[i] 记录以 i 开头,长度为 j 的字符串的排名。在这里,长度 j 1,故排名可以直接由 r[ ] 等价而来。

sa[ ] 数组含义见上, sa[i] 记录排名第 i 名字符串的开头位置,在计算过程中,排名是针对 [i,i+j1] 一段而言的。

x[ ] sa[ ] 互为逆运算。

第二部分

循环体

    for (j = 1, p = 1; p < n; j <<= 1, m = p) {
        //注意!每次循环要记得更新m的值!

当某一次循环结束后所有排名都不相同即可停止,即名次数等于 n .

计数排序的上限 m 每次更新为当前不同的名次数。

循环体内

  1. 1.
        for (p = 0, i = n-j; i < n; ++i) y[p++] = i;
        for (i = 0; i < n; ++i) if (sa[i] >= j) y[p++] = sa[i] - j;

我们知道,倍增算法的主要思想是基数排序,在这里,基数排序有两个关键字。

y[ ] 数组记录的是 以第二关键字排序后 第一关键字 的下标位置

y[i]=k 的含义是:排名第 i 的第二关键字,其下标为 k+j,而其对应的第一关键字下标即为 k (因为第二关键字在第一关键字之后 j 位置处)

具体操作上:

显然对于字符串的最后 j 位,它们的第二关键字均为 0,所以排在最前面的 j 个。

再从头到尾遍历一遍,将sa[ ]原顺序基本不变地放入 y[ ] (注意到它们含义的相似性)。其中,如果 sa[i]<j ,说明前面没有足够的长度给第一关键字,因此 i 位置不能作为一个合法的第二关键字的开始位置;否则记录下第一关键字的位置。

    2.
        for (i = 0; i < n; ++i) wv[i] = x[y[i]];

y[i]=k 的含义是:排名第 i 的第二关键字,其对应的第一关键字下标即为 k

x[i] 记录以 i 开头,长度为 j 的字符串的排名;

由此,我们得出, wv[i] 记录 排名第 i 的第二关键字,其对应的第一关键字的排名。也即第二关键字升序排序后对应的第一关键字。

3.

        for (i = 0; i < m; ++i) wt[i] = 0;
        for (i = 0; i < n; ++i) ++wt[wv[i]];
        for (i = 1; i < m; ++i) wt[i] += wt[i - 1];
        for (i = n-1; i >= 0; --i) sa[--wt[wv[i]]] = y[i];

再做一遍计数排序,使得在第二关键字升序的基础上,第一关键字也升序,将下标一个个填进去。

4.

        t = x, x = y, y = t, x[sa[0]] = 0;
        for (p = 1, i = 1; i < n; ++i, ++p) x[sa[i]] = cmp(y, sa[i], sa[i-1], j) ? p - 1 : p;

因为x[ ] sa[ ] 互为逆运算,所有可以由 sa[ ] 反推 x[ ] ,为下一次循环做准备。

因为可能有相同的字符串,所以并不是单纯的逆运算,还需要用 cmp() 处理一下排名相同的情况。

bool cmp(int* r, int a, int b, int l) { return r[a] == r[b] && r[a+l] == r[b+l]; }

只需比较第一关键字和第二关键字即可。(注意,这里的 r 不是初始字符串,传进去的参数事实上是 y,也即原来的 x ,即对应段字符串的排名)

第三部分

    for (i = 0; i < n; ++i) rk[sa[i]] = i;
    int k = 0;
    for (i = 0; i < n - 1; h[rk[i++]] = k) {
        //注意!这里上界是n-1而不是n,因为sa[n-1] = 0
        for (k = k ? --k : 0, j = sa[rk[i] - 1]; r[i+k] == r[j+k]; ++k);
    }

height 值,利用性质 h[i]h[i1]1 ,代码中的 h[ ] 即为 height[ ] k 即为 h[ ]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值