后缀数组(Suffix Array)

啥是后缀数组?难不难?

后缀数组看上去是一个很高深,很玄妙的东西。

但首先,我们要树立一个观点:它!不难!!

实际上,他也真的不难。难的只是其中一个想法的操作。除此以外,不难。

后缀数组的概念

首先,我们需要了解一下什么是后缀

后缀,顾名思义,缀在后面的东西。在字符串中,就是缀在字符串后面的东西,大家可以联系一下英语单词中的后缀来理解。

举个例子

字符串:abaab

后缀分别有:

abaab, baab, aab, ab, b

相信到这里,大家已经理解了后缀的概念。那么接下来,我们来看一下什么是后缀数组。

常用的后缀数组通常有以下几个数组变量(后缀数组的核心):

int sa[maxn];
int rank[maxn];

以下的排序指字符串按字典序进行排序。

sa数组:sa数组的下标和字符串的下标一一对应,sa[0]就表示substr(0, n)排完序之后的次序。

rank数组:和sa数组正好相反,rank[i]=j的含义是排名第i的字符串在所给字符串的位置j处(这里字符串的位置当然是指字符串第一个字符在所给字符串中的位置)

总结:sa 和 rank数组处于一种互补的状态,可以这么来区分两个数组,sa表示谁第几名,rank表示第几名的是谁。

好了,到这,后缀数组的概念就差不多了。

是不是很简单??

诶!别急!继续看下去!

如何获得sa和rank数组

可能很多小伙伴的第一想法就是建立一个String数组,把题目给的字符串的所有后缀子串存进去。再来一个sort快排就完事了。

这确实是一个方法,但是太暴力了。快排的时间复杂度是O(nlogn),但是我们字符串大小比较不同于数字的大小比较,字符串的大小比较也是一个O(n)的时间复杂度,这么一来,这个算法的时间复杂度就是O(n^2logn),很显然,这是不可取的!!

这也就是后缀数组的难点了,怎么样对后缀子串进行高效的排序,获得sa,rank数组的值。


在这,给大家介绍一种方法,倍增算法

算法的大致思想如下:

换一种排序的思路,不是真的对所有的后缀子串进行排序并交换位置(即最开始暴力算法中的sort)。而是通过一定的规则我们来计算后缀子串的排名。具体思路如下:


第一次排序:对每个后缀子串的前1个字符进行排序

第二次排序:对每个后缀子串的前1—2个字符进行排序

第三次排序:对每个后缀子串的前1—4个字符进行排序

第n次排序:对每个后缀子串的前1—2^(n-1)个字符进行排序


????这啥意思???为什么这么做??

上图!!

在这里插入图片描述

图片其实也有一点点抽象,对着图片和下面的文字我们来理解一下。

倍增,可以理解为每次比较的字符个数,是按照2的指数增长的(也就是2倍增)。

为什么采取这种措施?

比方说我们现在要进行第三次排序,也就是对前4个字符进行排序,那么在此之前的第二次排序,我们一定已对后缀字符的前2个字符进行了排序(再次强调!这里的排序是“假“的!只是通过比较获得次序而已)

所以此时,我们可以通过第二次排序的结论来进行第三次排序。

计算任一后缀子串k的前4个字符的次序号z可以通过比较后缀子串k的前2个字符的次序号x和后缀子串k+2的前两个字符的次序号y,进而得到一个而元祖(x,y),这是我们后面进行比较排序的资本!

计算任一后缀子串k的前4个字符的次序号z可以通过比较后缀子串k的前2个字符的次序号x和后缀子串k+2的前两个字符的次序号y,进而得到一个而元祖(x,y),这是我们后面进行比较排序的资本!

计算任一后缀子串k的前4个字符的次序号z可以通过比较后缀子串k的前2个字符的次序号x和后缀子串k+2的前两个字符的次序号y,进而得到一个而元祖(x,y),这是我们后面进行比较排序的资本!

大家对照图片好好理解一下这句话!

获得了这个资本之后呢,我们就可以进行排序啦。

比如:

若两个后缀子串的而元祖分别为(x1, y1) (x2, y2), 排序分别为z1,z2

若 x1<x2,则z1小(排名更靠前),x1= =x2 && y1 < y2, z1更小, x1= =x2 && y1= = y2, z1= =z2

代码:

#include <iostream>
#include<algorithm>
#include<cstring>
using namespace std;
#define maxn 1000009
string str;//给定的字符串
int n;//字符串的大小
int Sa[maxn];//所求的后缀数组
int Rank[maxn];//表示后缀排第几的数组
int tmpRank[maxn];//临时存储Rank的值
int k=0;
bool cmp(int x,int y)//
{
    if(Rank[x] != Rank[y])//先比较x
        return Rank[x] < Rank[y];
    //比较y,如果超出n则补为-1,防止数组越界
    int rx = x+k>n ? -1 : Rank[x+k];
    int ry = y+k>n ? -1 : Rank[y+k];
    return rx < ry;   //相当于return Rank[x+k] < Rank[y+k];不过多加了一个防越界的操作
}
bool judge(int str1,int str2)//判断后缀子串x,y的二元组是否相等
{
    if(Rank[str1]==Rank[str2])//先比较x
    {
        //比较第y,如果超出n则补为-1,防止数组越界
        int r1 = str1+k>n ? -1 : Rank[str1+k];
        int r2 = str2+k>n ? -1 : Rank[str2+k];
        if(r1==r2)  return true;
    }
    return false;
}
void getSa()
{
    memset(Rank, 0, sizeof(Rank));
    //初始化
    for(int i = 1; i <= n; i++) {
        Sa[i] = i;
        Rank[i] = str[i];//第一次的Rank就是后缀的第一个字符,字符的大小就可代表后缀的Rank
    }
    for(k = 1; k <= n; k <<= 1) {
        sort(Sa+1, Sa+n+1, cmp);//根据二元组排序
        int number = 0;//number表示目前的最大次序号
        for(int i = 1; i <= n; i++)//按照Sa从小到大给后缀更新次序
        {
            if(judge(Sa[i], Sa[i-1]) == 0)//如果与前一个二元组不相同,则产生新的次序号,将number+1
                number++;
            tmpRank[Sa[i]] = number;
        }
        for(int i = 1; i <= n; i++)
            Rank[i] = tmpRank[i];
        if(number >= n) //排序结束,提前退出循环
            break;
    }
    return ;
}
int main()
{
    cin>>str;
    n=str.size();
    str=" "+str;//使字符串编号是1到n
    getSa(); //计算获得sa数组
    for(int i = 1; i <= n; i++)
        cout<<Sa[i]<<" ";
    return 0;
}
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值