后缀数组实现及模板详解

写在前面

首先要知道后缀数组的作用:将字符串的每一个后缀按照字典序进行排序,当然我们还可以进行其他的操作们可以求解相邻两个后缀之间最长的公共前缀的长度(LCP问题)

后缀数组

首先我们要明白基数排序,如果不明白的话可以看一下我的另一篇博客:基数排序的实现

当我们明白了基数排序之后,我们就可以看一下后缀数组的模板了:

//不怕别人比你聪明,就怕别人比你聪明还比你努力
#include
#include
#include
#include
#include
#include
#include 
#include 
#include 
#include
#include 
#define INF 0x3f3f3f3f
#define ll long long

using namespace std;
const int MAXN = 500;
int n,w;
char str[MAXN];
int SA[MAXN];
int buc[MAXN];
int x[MAXN],y[MAXN],len;
//x数组表示第一元素,y数组表示第二元素

void da()
{
    //SA表示我排在第几位的是谁,SA[k] = i,排在第k位的是第i个后缀
    scanf("%s",str);
    len = strlen(str);
    int m = 127;//m表示第一次我们字符串的范围
    //在进行倍增之前首先对每一个元素进行一次基数排序,然后放到数组SA中
    for(int i =0 ;i < m;i ++) buc[i] = 0;
    for(int i =0 ;i < len;i ++) buc[x[i] = str[i]] ++;
    for(int i = 1;i < m;i ++) buc[i] += buc[i-1];
    for(int i = len-1;i >= 0;i --) SA[ -- buc[x[i]]] = i;
    for(int k = 1;k <= len; k <<= 1)    //进行倍增
    {
        int p = 0;
        for(int i = len - 1; i >= len -k; i --) y[p++] = i;    //我们要得到第二元素,因为要向后移动k个,所以这里表示的是添零操作
        //因为零一定会排在最前面,所以y[p++] = i;表示排在第p位的是第i个后缀,表示的就是最后面的后缀
        for(int i = 0;i < len;i ++) if(SA[i] >= k) y[p++] = SA[i] - k;   //然后我们删除前面几个跳过的元素,这些跳过的元素都是在k之前的,也就是SA[i] < k
        //比如我们要排序的数是:92,81,30,24,57,96;
        //我们得到的y数组是对第二元素进行的排序,即:y[] = 3,2,1,4,6,5
        //x[y[]] = 30,81,92,24,96,57  :即表示对第二元素进行排序的结果
        //下面的排序就和只有一个元素的排序相同了

        for(int i =0 ;i < m;i ++) buc[i] = 0;        //再进行一次基数排序
        for(int i = 0;i < len;i ++) buc[x[y[i]]] ++;
        for(int i = 1;i < m;i ++) buc[i] += buc[i-1];
        for(int i = len-1;i >= 0;i --) SA[ -- buc[x[y[i]]]] = y[i];

        swap(x,y);  //交换之后现在y表示每个位置上现在的数字
        //SA表示现在排在第几位置上的是谁
        p = 1;x[SA[0]] = 0;  //首先是将排在第0位置上的
        for(int i = 1;i < len;i ++)   //这里我们要更新最大的范围,只有当前面和后面不相同的时候我们才进行p++操作,否则表示上一个数
        {
            if(y[SA[i-1]] == y[SA[i]] && y[SA[i-1]+k] == y[SA[i] + k])
                x[SA[i]] = p-1;
            else
                x[SA[i]] = p++;
        }//当
        if(p >= len)//当每一个元素都是不同的排名的时候,可以结束循环
            break;
        m = p;//更新范围
    }
}
int main()
{
    da();
    //最终我们得到的后缀数组内容就在SA数组中,
    for(int i=0; i

我们这样就可以得到后缀的基本排序,我们后面看一下后缀数组的基本应用

后缀数组的基本应用

后缀数组中有很多应用,这里我们就先说一个应用:最长公共前缀,其余的应用我会在遇到一些题中说。

我们这里引入一些辅助数组:rank[] :表示从第i个字符开始的后缀排名第几,这里和上面的SA数组的表示正好是相反的。

height[]:我们定义height[i] = suffix(sa[i-1]) 和 suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。这里suffix(i)表示从i开始到结尾这个后缀

那么对于j和k,我们设rank[j]  < rank[j],则具有以下性质:

suffix(j) 和 suffix(k)的最长公共前缀为 height[rank[j] + 1],height[rank[j]+2]......height[rank[k]]中的最小值

注:那么我们可以得到两个前缀的最长公共前缀需要求解一个区间的最小值,那么这里就是我们的RMQ问题,可以看一下另一博客:RMQ问题原理及实现

但是我们还是要看一下我们应该怎么求解height数组,这里我们有要用到一个辅助数组,以及这个辅助数组的一个性质。

h[]:定义h[i] = height[rank[i]],也就是suffix(i)和它前一名的后缀的最长公共前缀

h数组有以下性质:                                               h[i] >= h[i-1]+1

我们按照h[1],h[2],h[3]......的顺序求解,并且利用上面的性质,我们可以将求解height数数组的时间复杂度降为O(n)

我们首先计算出rank数组

for(int i =0 ;i < len;i ++) rank[SA[i]] = i;

然后我们按照h[1],h[2],h[3]...的顺序计算height数组

void get_height()
{
    for(int i =0 ;i < len;i ++) rank[SA[i]] = i;
    int k = 0;
    for (int i = 0; i < len; i++)
    {
        if (rank[i] == 0) {height[0] = 0; continue;} // 当排名为0时,因为他前面没有字符串,所以这里就应该定为0
        if (k) k--; // 从 k - 1 开始推,这里我们就利用了上面h数组的性质
        int j = SA[rank[i] - 1];   //得到上一名的开始位置
        while (str[i + k] == str[j + k] && i + k < len && j + k < len) k++;//相同的话,我们就一直向后加
        height[rank[i]] = k; //更新height数值
    }
}

最终我们得到完整的程序

//不怕别人比你聪明,就怕别人比你聪明还比你努力
#include
#include
#include
#include
#include
#include
#include 
#include 
#include 
#include
#include 
#define INF 0x3f3f3f3f
#define ll long long

using namespace std;
const int MAXN = 500;
int n,w;
char str[MAXN];
int SA[MAXN];
int buc[MAXN];
int x[MAXN],y[MAXN],len;
int rank[MAXN],height[MAXN];///rank[i] 用来记录后缀 i 在 SA 数组的位置,表示字符串i的排名
///height[i] 记录后缀 SA[i] 和 SA[i - 1] 的 LCP 的长度。
int h[MAXN]; //h[i] = height[rank[i]]:排名为i的后缀与排名靠前一位的最长公共前缀

//rank:这个排在第几,SA:排在第几的是谁
void da()
{
    scanf("%s",str);
    len = strlen(str);
    int m = 127;
    for(int i= 0 ;i < m;i ++) buc[i] = 0;
    for(int i = 0;i < len;i ++) buc[x[i] = str[i]] ++;
    for(int i = 1 ;i < m;i ++) buc[i] += buc[i-1];
    for(int i = len -1;i >= 0;i --) SA[-- buc[x[i]]] = i;
    for(int k = 1;k <= len;k <<= 1)
    {
        int p = 0;
        for(int i = len- 1;i >= len - k;i --) y[p++] = i;
        for(int i =0 ;i < len;i ++) if(SA[i] >= k) y[p++] = SA[i] - k;

        for(int i= 0 ;i < m;i ++) buc[i] = 0;
        for(int i =0 ;i = 0;i -- ) SA[ -- buc[x[y[i]]]] = y[i];

        swap(x,y);
        p = 1;x[SA[0]] = 0;
        for(int i = 1;i < len;i ++)
        {
           if(y[SA[i-1]] == y[SA[i]] && y[SA[i-1]+k] == y[SA[i]+k])
                x[SA[i]] = p-1;
           else
                x[SA[i]] = p++;
        }
        if(p >= len) break;
        m = p;
    }
}

//如果直接求的话,复杂度为O(n^2),但是我们按照h[1],h[2],h[3]....的顺序求解,并利用h数组的性质,时间复杂度可以降为O(n)
//h[i] >= h[i-1]+1                   h[i] = height[rank[i]]

void get_height()
{
    for(int i =0 ;i < len;i ++) rank[SA[i]] = i;
    int k = 0;
    for (int i = 0; i < len; i++)
    {
        if (rank[i] == 0) {height[0] = 0; continue;} // 第一个后缀的 LCP 为 0。
        if (k) k--; // 从 k - 1 开始推
        int j = SA[rank[i] - 1];
        while (str[i + k] == str[j + k] && i + k < len && j + k < len) k++;
        height[rank[i]] = k;
    }
}

int main()
{
    da();
    get_height();
    cout<<"height: ";
    for(int i =0 ;i < len;i ++)
        cout<

后记

这个算法真的是看了三四天才看明白,并且上面整理的也是在自己刚看明白的时候整理的,如果这里面有写的很烂的地方或者有错误的地方,请在下面评论,我会及时进行更改,并且后面对后缀数组有了更深的理解,我会及时过来进行修正。谢谢!

参考文献

知乎后缀数组(Suffix Array):https://zhuanlan.zhihu.com/p/21283102

后缀数组详解+模板:https://www.cnblogs.com/thmyl/p/6296648.html








你可能感兴趣的:(后缀数组)