后缀数组

后缀数组

后缀数组是处理字符串问题的有力工具。


一.写在前面的话

告别严谨枯燥的数学证明,
告别头绪繁多的“算法类比”
告别大佬神仙的各种专业术语,
举例让你感性理解。
笔者将浅略地讨论倍增法求解后缀数组。如果你不知道倍增,也不知道后缀数组,那么恭喜,你来对了!

二.计数排序

不要忽视这部分,我当年就卡在这儿

针对如下问题:

给定:n个数的关键字key,和一个长度为n的指针数组ord
保证:所有关键字key均小于等于1百万,ord是n的一个排列
要求对ord进行稳定的排序,使得任取i,有
    key[ord[i]]≤key[ord[i+1]]
    “稳定”的含义是:当上式取等时,ord和ord[i+1]的相对位置不变

输出排序后的数组sa

样例输入:

key0 key1 key2 key3 key4 key5 key6 key7
97 97 98 97 97 97 97 98

{ 7 0 2 3 4 5 1 6 }= ord

样例输出:

{ 0 3 4 5 1 6 7 2  }=sa

表示

最小的是key0,然后是key3,紧接着是key4....为什么跳过key1呢?因为在ord中0、3、4都比1出现的早。

说白了,就是给定一个指针数组和每个指针所指向的关键字,要求对这些指针进行稳定的排序。

排序前 ord 7(98) 0(97) 2(98) 3(97) 4(97) 5(97) 1(97) 6(97)
排序后 sa 0(97) 3(97) 4(97) 5(97) 1(97) 6(97) 7(98) 2(98)

如果关键字小于等于1百万,那么
我们可以用计数排序在O(1百万)的时间内解决。步骤如下:


第一步
各种初始化
第二步
我们计算出每个关键字出现的次数,记作f。
样例中,f[97]=6,f[98]=2。值得注意的是,f在将来要排好序的sa数组上,还有着"区间长度"的意义。
排序后 sa 0(97) 3(97) 4(97) 5(97) 1(97) 6(97) 7(98) 2(98)
* * * * * * + +

: 上图中星号区间长度为f[97],加号区间长度为f[98]

第三步

计算f的前缀和,用前缀和更新f,
样例中f[97]=6,f[98]=8
那么f在sa中的意义就变为

指针,准确地说,是
指针数组的队尾指针

位置 0 1 2 3 4 5 6=f[97] 7 8=f[98]
排序后 sa 0(97) 3(97) 4(97) 5(97) 1(97) 6(97) 7(98) 2(98)
97的空队尾(亦98的实队头) 98的空队尾
第四步

这一步最复杂,我们要逆序扫描原指针数组ord,把所有ord放入sa中。

样例中,我们已知

排序前 ord 7(98) 0(97) 2(98) 3(97) 4(97) 5(97) 1(97) 6(97)

: 那我们依次把6(97)、1(97)、5(97)。。。放进sa中

位置 0 1 2 3 4 5 6=f[97] 7 8=f[98]
排序后 sa 未知 未知 未知 未知 未知 未知 未知 未知
位置 0 1 2 3 4 5=f[97] 6 7 8=f[98]
排序后 sa 未知 未知 未知 未知 未知 6(97) 未知 未知
位置 0 1 2 3 4=f[97] 5 6 7 8=f[98]
排序后 sa 未知 未知 未知 未知 1(97) 6(97) 未知 未知
位置 0 1 2 3=f[97] 4 5 6 7 8=f[98]
排序后 sa 未知 未知 未知 5(97) 1(97) 6(97) 未知 未知

: 最后就得到了答案

代码实现:
简直一步行代码,刚刚好4行
void SORT()
{
    for(i=0;i<=m;i++) f[i]=0;
    for(i=0;i<n;i++) f[key[ord[i]]]++;
    for(i=0;i<=m;i++) f[i]+=f[i-1];
    for(i=n-1;i>=0;i--)
        sa[--f[key[ord[i]]]]=ord[i];
}

总结一下
给定一个指针数组ord和每个指针所指向的关键字key,要求对这些指针进行稳定的排序,输出排序后的数组sa。
如果最大关键字不超过1百万,通过计数排序就可以在O(1百万)的时间内完成排序

以下我将简称这个过程为

以key为关键字对ord进行计数排序


三、双关键字的计数排序

大佬们也喜欢称它为基(ji)数(lao)排序。事实上,基数排序是多关键字的计数排序,应用肯定比双关键字广泛。不够对于后缀数组来说,卖个关子,你仅需知道双关键字就够了

针对如下问题

给定n个数对(key1[0],key2[0])(key1[1],key2[1])...(key1[n-1],key2[n-1])
保证所有关键字小于等于1百万
要求对n个数对双关键字排序(其实也可以不稳定),然后
输出一个数组sa,sa[i]表示第i大的数对在原“数对数组”的位置(即对指针排序)

样例输入:

位置0 位置1 位置2 位置3 位置4 位置5 位置6 位置7
0(97,97) 1(97,98) 2(98,97) 3(97,97) 4(97,97) 5(97,97) 6(97,98) 7(98,0)

样例输出:

0(97,97) 3(97,97) 4(97,97) 5(97,97) 1(97,98) 6(97,98) 7(98,0) 2(98,97)

解法就是,令指针数组sa[i]=i,然后以key2位关键字对sa计数排序,接着以key1为关键字对sa再计数排序,最后输出sa即可。不多说,放代码:

void SORT()
{
    for(i=0;i<=m;i++) f[i]=0;
    for(i=0;i<n;i++) f[key[ord[i]]]++;
    for(i=0;i<=m;i++) f[i]+=f[i-1];
    for(i=n-1;i>=0;i--)
        sa[--f[key[ord[i]]]]=ord[i];
}
int main()
{
    cin>>key1>>key2;//皮一下
    for(i=0;i<n;i++)
        key[i]=key2[i],
        ord[i]=i;
    SORT();
    for(i=0;i<n;i++)
        key[i]=key1[i],
        ord[i]=sa[i];
    SORT();
    cout<<sa;//再皮一下
}

四、后缀排序

我们将走进字符串的世界,准备好了吗?

如表,s="aabaaaab"有8个后缀,(显然有36个非空子串)

位置 0 1 2 3 4 5 6 7
字符串s a a b a a a a b
后缀0 a a b a a a a b
后缀1 a b a a a a b
后缀2 b a a a a b
后缀3 a a a a b
后缀4 a a a b
后缀5 a a b
后缀6 a b
后缀7 b

我们说“后缀i”实际上是在说“以s[i]为首的后缀”。换言之,i,是一个指针,指向一个后缀(字符串)。考虑这么一个问题:

 给定一个字符串s,长度为n,
 要求你将0,1,2..n-1,这n个指针以“后缀i”为关键字排序,
 然后输出排序后的指针数组sa
 PS.两个后缀是依据字典序比较大小的,由于任意两个后缀长度不等,所有任意两个后缀不可能相等。故输出唯一

样例输入:

aabaaaab

样例输出:

3 4 5 0 6 1 7 2

样例解释:

位置 0 1 2 3 4 5 6 7
字符串s a a b a a a a b
后缀3 a a a a b
后缀4 a a a b
后缀5 a a b
后缀0 a a b a a a a b
后缀6 a b
后缀1 a b a a a a b
后缀7 b
后缀2 b a a a a b

这里只给一个nlgn的倍增法。实际上还有O(n)的DC3算法,O(n)的SA-IS算法,还有O(nlg^2)的哈希。不再赘述


后缀排序最重要的是一个数组rk[],代表估价(或是排名rank),其余的内容均是计数排序

现在,我们考虑n个长为2的子串,为了方便,我们在最后一个子串上补0

位置 0 1 2 3 4 5 6 7
字符串s a a b a a a a b
子串0 a a
子串1 a b
子串2 b a
子串3 a a
子串4 a a
子串5 a a
子串6 a b
子串7 b 0

把它们排序,实际上是个双关键字的计数排序。排完序后,我们可以把排名看作这个子串的“估价”。发现问题的可分解性。具体地:

规定长度为1的子串的估价为其ascll码,那么
长度为2的子串i的估价,可以看作一个二元组(前半段的估价,后半段的估价)在所有长度为2的n个子串中的排名。

同理

长度为4的子串i的估价,可以看作一个二元组(前半段的估价,后半段的估价)在所有长度为4的n个子串中的排名。

不断地倍增,会得到所有后缀的估价(排名)
后缀数组
我下面放两个代码,第一个可读性好,但是常数大;第二个把常数优化了一下(实际上只是换个头文件,去掉子函数罢了)。

//这份代码可读性好,但是常数大 
#include <bits/stdc++.h>
using namespace std;

const int maxn=1e6+2;
int f[maxn];//作为临时数组 
int key[maxn],ord[maxn],sa[maxn];//计数排序? 
int rk[maxn],tp[maxn];//rk和tp本质上是一样的,tp是rk的倍增 
char s[maxn];//输入的串 
int n,i,j,m;

bool dif(int i,int j){return rk[i]!=rk[j]||tp[i]!=tp[j];}//两个个二元数对中,只要有一组关键字不同,两个数对就是不同 
void SORT()//计数排序 
{
    for(i=0;i<=m;i++) f[i]=0;
    for(i=0;i<n;i++) f[key[ord[i]]]++;
    for(i=0;i<=m;i++) f[i]+=f[i-1];
    for(i=n-1;i>=0;i--)
        sa[--f[key[ord[i]]]]=ord[i];
}

void get_sa()
{
    int i,k;
    for(i=0;i<n;i++) rk[i]=s[i];
    for(k=1;k<n;k<<=1)
    {
        for(i=0;i<n;i++)
            tp[i]=(i+k<n)?rk[i+k]:0;
        for(i=0;i<n;i++)
            key[i]=tp[i],
            ord[i]=i;
        SORT();
        for(i=0;i<n;i++)
            key[i]=rk[i],
            ord[i]=sa[i];
        SORT();
        
        f[sa[0]]=1;//注意必须是1而不是0。0是用来补的
        for(i=1;i<n;i++)
            f[sa[i]]=f[sa[i-1]]+dif(sa[i],sa[i-1]);
        for(i=0;i<n;i++)
            rk[i]=f[i];
        m=n;//这一行不能省,尤其n比m大的时候 
    }
}

int main()
{
    scanf("%s",&s);
    n=strlen(s);
    m=128;//字符集大小 
    get_sa();
    for(i=0;i<n;i++)
        printf("%d ",sa[i]);
    return 0;
}
//这份代码常数小一点 
#include<cstdio>
#include<cstring>
#include<algorithm>//这个头文件比bits快多了 
using namespace std;

const int maxn=1e6+2;
int f[maxn];
int key[maxn],ord[maxn],sa[maxn];
int rk[maxn],tp[maxn];
char s[maxn];
int n,i,j,m;

//这边不再设子函数 
void get_sa()
{
    int i,k;
    for(i=0;i<n;i++) rk[i]=s[i];
    for(k=1;k<n;k<<=1)
    {
        for(i=0;i<n;i++)
            tp[i]=(i+k<n)?rk[i+k]:0;
        //for(i=0;i<n;i++)printf("(%d,%d)\n",rk[i],tp[i]);
            
        memset(f,0,sizeof(f));
        for(i=0;i<n;i++) f[tp[i]]++;
        for(i=0;i<=m;i++) f[i]+=f[i-1];
        for(i=n-1;i>=0;i--)
            ord[--f[tp[i]]]=i;
            
        memset(f,0,sizeof(f));
        for(i=0;i<n;i++) f[rk[i]]++;
        for(i=0;i<=m;i++) f[i]+=f[i-1];
        for(i=n-1;i>=0;i--)
            sa[--f[rk[ord[i]]]]=ord[i];
        
        f[sa[0]]=1;
        for(i=1;i<n;i++)
            f[sa[i]]=f[sa[i-1]]+(rk[sa[i]]!=rk[sa[i-1]]||tp[sa[i]]!=tp[sa[i-1]]);
        for(i=0;i<n;i++)
            rk[i]=f[i];
        m=rk[sa[n-1]]; 
    }
}

int main()
{
    scanf("%s",&s);
    n=strlen(s);
    m=128;
    get_sa();
    for(i=0;i<n;i++)
        printf("%d ",sa[i]);
    return 0;
}

强烈建议你在往下读之前,先刷一遍模板题
洛谷P3809 【模板】后缀排序

未完待续。。