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

来源:互联网 发布:mac 安装oracle数据库 编辑:程序博客网 时间:2024/05/01 10:41

参考

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 的字符串的排名。在这里,长度 j1,故排名可以直接由 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.
        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[ ]