后缀数组的正确性证明

后缀数组的正确性证明

后缀数组的一些定义

定义1:一个从下标1开始长度为n字符串,从第i位到第n位组成的字串称为此字符串的第i个后缀suf(i)函数表示该字符串的第i个后缀
定义2:将字符串所有后缀字串从小到大排序,排名第i的字串被称为排名第i的后缀lev(i)函数表示该字符串的排名第i的后缀
定义3:lcp(i,j)函数表示lev(i)与lev(j)最长公共前缀的长度。
定义4:数组sa,有:排名第i的后缀第sa[i]个后缀。即lev(i)=suf(sa[i])。
定义5:数组rk,有:第i个后缀是排名rk[i]个后缀。即lev(rk[i])=suf(i)。
定义6:数组height,有:lcp(i,i-1) = height[i] (i>=2)。

sa数组的建立

类桶排序

给定一些范围在[1,n]的数字,使用cnt数组记录每个数字出现的次数,求cnt前缀和数组s。可知s[i]表示小于等于i的数的个数。若无重复数字,s[i]即i的排名;若有重复数字,每次遇到i使s[i]-1即可。为保证稳定排序,需要从后往前排序。

基数排序

将整数按位数切割成不同的数字,然后按每个位数分别比较:
① 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
② 从最低位开始,依次进行一次类桶排序。
③ 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

依照key排序

定义7:长度为n的字符串,从前往后每k个字符一组,分割成n/k个字串(长度不够,用空字符拼凑)。从前往后,第i个子串的称为长度为k的第i关键字(key)。

使用倍增的思想,k从1开始,每轮依照长度为2*k的第1关键字把字符串所有后缀基数排序,具体而言:先依照长度为k的第2关键字类桶排序,再依照长度为k的第1关键字类桶排序,最后k翻倍,进行下一轮直到k大于n。

一些细节:

void get_sa(void)
{
    n = strlen(s+1), m = 'z';
    // 依照首字母预处理k1和sa数组
    for(int i=1;i<=n;i++) cnt[k1[i] = s[i]] ++;
    for(int i=2;i<=m;i++) cnt[i] += cnt[i-1];
    for (int i = n; i; i -- ) sa[cnt[k1[i]] -- ] = i;
    
    for(int k=1; k<=n; k<<=1)
    {
        int num = 0;
        // n-k+1之后的后缀无第2关键字,认为第2关键字最小
        for(int i=n-k+1;i<=n;i++) k2[++num] = i;
        // 上一轮先后依据长为k/2的第1第2关键字排序,即依据长为k的第1关键字排序
        // sa存储其结果,sa[r] = i,表示第i个后缀依据长为k的第1关键字排名为r
        // 即第sa[r]-k个后缀依据长为k的第2关键字排名为r
    
        for(int r=1;r<=n;r++)
            if(sa[r] > k) // sa[r]之前要有长为k的第1关键字
                k2[++num] = sa[r] - k;
                
        // 依照第一关键词排序
        // 上一轮先后依据长为k/2的第1第2关键字排序,即依据长为k的第1关键字排序
        for(int i=1;i<=m;i++) cnt[i] = 0;
        for(int i=1;i<=n;i++) cnt[k1[i]] ++;// k1[i]存储的是上一轮suf(i)离散化后的值
        for(int i=2;i<=m;i++) cnt[i] += cnt[i-1];
        // k2数组从后向前扫,即先依照第2关键字排序
        for(int r=n;r;r--) sa[cnt[k1[k2[r]]] --] = k2[r], k2[r] = 0;
        // 更新k1数组,并离散化
        swap(k1, k2);
        k1[sa[1]] = 1, num = 1;
        // 如果sa[r]与sa[r-1]前2*k个字符相等
        //则suf[sa[r]]与suf[sa[r-1]] 和 suf[sa[r]+k]与suf[sa[r-1]+k]的长为k的第1关键字分别相等
        for(int r=2;r<=n;r++)
            k1[sa[r]] = (k2[sa[r]] == k2[sa[r-1]] && k2[sa[r]+k] == k2[sa[r-1]+k]) ? num : ++ num;
        // 如果所有后缀依照某个前缀排序互不相等,则顺序不会再改变了
        if(num == n) break;
        m = num;
    }
}

rk与height数组的建立

rk数组使sa数组的反数组,故可快速求出:

    for(int r=1;r<=n;r++) rk[sa[r]] = r;

height[i] = lcp(i,i-1),若暴力求解效率低下,这里有个技巧。

lcp(i,j)

性质:lcp(i,j) = min(lcp(i,k), lcp(k, j)) (i < k < j)

证明:
把lev(i),lev(k),lev(j)分别分成两大部分,前一部分为三者的最长公共前缀。
可以证明lev(i)与lev(j)的后一部分没有相同的前缀。若lev(i)与lev(j)的后一部分有相同的前缀,又lev(k)的后一部分大于lev(i),则lev(k)的后一部分大于lev(j),这与k < j矛盾。
故三者的前一部分,即三者的最长公共前缀长度为lcp(i,j)。

推广:lcp(i,j) = min( lcp(i,i+1), lcp(i+1, i+2) , ... , lcp(j-1, j)) (i < j)

h(i)

定义8:h()函数,有:h(i) = height[ rk[i] ]。
性质: h(i) >= h(i-1) - 1

证明:
不妨设sa[i] > sa[i-1]
①:rk[i-1] =1 或 h(i-1) = 0,h(i) >= 0 成立
②:h(i-1) >= 1,令sa[rk[i-1]-1] = x, 则h(i-1) = height[rk(i-1)] = lcp(rk[i-1], rk[x])。
由于suf(i-1)与suf(x)有相同的前缀,且rk(i-1) > rk(x), 则各去掉一个首字符后,suf(i) >= suf(x+1), 即rk[i] >= rk[x+1]。
而lcp(rk[i], rk[x+1]) = min(lcp(rk[x+1], rk[x+1]+1), ..., lcp(rk[i]-1), lcp(rk[i])), 可知lcp(rk[i], rk[x+1]) >= lcp(rk[i]-1), lcp(rk[i])。
此时lcp(i, x+1) = h(i-1) - 1;h(i) = lcp(rk[i], rk[i]-1),可知h(i) >= h(i-1) - 1 。
综上h(i) >= h(i-1) - 1。

故,考虑从前往后枚举每个后缀,如枚举到suf(i), 可知h(i) = height[rk[i]] = lcp(rk[i], rk[i] - 1 )。令sa[rk[i]-1] = x。而h(i) >= h(i-1) - 1,所以lcp(rk[i], rk[x]) >= h(i-1) - 1。故求h(i)时,可以从h(i-1)开始枚举比较suf[i]和suf[x]是否相同。
代码:

void get_height(void)
{
    for(int r=1;r<=n;r++) rk[sa[r]] = r;
    for(int i=1, h=0;i<=n;i++)
    {
        // height[1] = 0
        if(rk[i] == 1) continue;
        if(h) h --;
        int x = sa[rk[i]-1];
        // 枚举h位之后
        while(i+h <= n && x+h <= n && s[i+h] == s[x+h]) h ++;
        height[rk[i]] = h;
    }
}

posted @ 2022-08-04 15:33  DarkLights  阅读(7)  评论(0编辑  收藏  举报