后缀数组——罗穗骞倍增算法代码详解

首先解释一下用到的几个数组。

数组sa:构造完成前表示关键字数组,下标表示名次,值表示关键字的首字符位置,值相同的时候名次根据在原串中相对位置的先后决定;构造完成后表示后缀数组,下标表示名次,值表示后缀的首字符位置。

数组x:表示rank数组,下标表示关键字的位置,值表示关键字大小(rank),相同的值有相同的rank。初始化为字符串r的每个字符大小(此时x并不代表rank,只借助其值比较相对大小)。在每次迭代后,根据sa重新赋值,并代表rank。

数组y:排序后的第二关键字数组,下标表示名次,值代表第二关键字的首字符位置。


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

这段代码是计数排序,是对每个字符排序用来初始化数组sa和x。这段代码不难懂,原理我就不讲了,重点在于这个计数排序有个特点:

先根据x的值排序,x值相等时根据出现先后次序排序。

    for(j = 1,p = 1; p < n ; j <<= 1,m = p)
进入循环,开始构造后缀数组,j表示关键字长度。


        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;
这里是对第二关键字排序。由于第一关键字和第二关键字都是长度为j的关键字,所以这里完全可以使用之前已经算好的第一关键字,但是第二关键字和第一关键字有些不同。看图。

后缀数组——罗穗骞倍增算法代码详解_第1张图片

上面的蓝线指向的是一个关键字。以长度为4时为例,下标超过4(图中rank为2)时的关键字将不存在第二关键字。所以从n-j到n的关键字的第二关键字都应该为0,排序的时候应该放在前面。下面这段代码就表达了这样的意思。

        for(p = 0, i = n - j; i < n; i++) 
            y[p++]=i;

其余部分可以利用第一关键字的排序进行。但是第二关键字的下标和第一关键字的下标是不一样的,第一关键字的值对应下标就是该关键字的下标,第二关键字的值的下标减去j才是对应关键字的下标。举个例子,上面的第一个组合关键字是(4,2)下标为0。其中4是第一关键字下标0;2是第二关键字下标是4,需要减去长度4才能对应到组合关键字的下标。

        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++)
            ws[i] = 0;
        for(i = 0; i < n; i++)
            ws[wv[i]]++;
        for(i = 1; i < m; i++)
            ws[i] += ws[i-1];
        for(i = n-1; i >= 0; i--)
            sa[--ws[wv[i]]] = y[i];

这段代码还是计数排序,其中这句wv[i] = x[y[i]];是最让人费解的。我认为这也是这段代码最巧妙所在。

首先思考一下接下来我们应该干什么?很明显是根据合并第一第二关键字,然后排序。

先根据第一关键字排序,第一关键字相等时根据第二关键字大小排序。

但是这段代码看上去并没有这么做,它只是做了一个计数排序而已。

还记得这个计数排序的特点:先根据x的值排序,x值相等时根据出现先后次序排序。

x里面存了上次关键字的排序,在本次排序中即是第一关键字的排序,x的值排序==第一关键字排序,这里的计数排序做的是对的。那么第二关键字呢?

前面对第二关键字进行了排序,在这里wv[i] = x[y[i]];根据第二关键字的顺序重新改变了第一关键字的顺序,也就是说在本次计数排序中,出现先后次序排序==第二关键字大小排序

换句话说,我们先单独对第二关键字排序,根据这个顺序改变第一关键字的顺序,由于在计数排序时首先按照第一关键字的值排序,而第一关键字的值没有改变所以首先还是根据第一关键字排序,改变的是第一关键字相同的时候,出现在前面的第二关键字排在前面。

做到这里就完成了第一第二关键字的合并,得到了合并以后的关键字顺序,它可以用于下次迭代。

每次新的迭代要用到rank数组x,由于有了关键字排序数组sa,要得到rank数组也很容易。但是对于相同的值,rank应该相同,所以要判断一下合并以后的关键字是否相同。这里都很好懂,我就不细讲了。

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


代码完整版:

int wa[maxn],wb[maxn],wv[maxn],ws[maxn];
 
int cmp(int *r , int a, int b, int l)
{
    return r[a] == r[b] && r[a+l] == r[b+l];
}
void da (int *r , int *sa , int n, int m)
{
    int i, j, p, *x = wa, *y = wb , *t;
    for(i = 0; i < m; i++) 
        ws[i] = 0;
    for(i = 0; i < n; i++) 
        ws[x[i] = r[i]]++;
    for(i = 1; i < m; i++) 
        ws[i] += ws[i-1];
    for(i = n-1; i >= 0; i--) 
        sa[--ws[x[i]]] = i;
    for(j = 1,p = 1; p < n ; j <<= 1,m = p)
    {
        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++)
            ws[i] = 0;
        for(i = 0; i < n; i++)
            ws[wv[i]]++;
        for(i = 1; i < m; i++)
            ws[i] += ws[i-1];
        for(i = n-1; i >= 0; i--)
            sa[--ws[wv[i]]] = y[i];
        for(t = x,x = y,y = t,p = 1,x[sa[0]] = 0,i = 1; i < n;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
    }
}


你可能感兴趣的:(ACM)