算法学习:后缀数组(SA)

【参考博客】

https://xminh.github.io/2018/02/27/%E5%90%8E%E7%BC%80%E6%95%B0%E7%BB%84-%E6%9C%80%E8%AF%A6%E7%BB%86(maybe)%E8%AE%B2%E8%A7%A3.html

 

 


 

【定义】

 

【后缀】从第i位到字符串结尾的子串

  


【解决问题】

 

从而解决

...................在字符串中找子串

...................比较子串关系

...................查找不同子串的数目

 

一般来说都是解决

字符串和子串关系的问题

 


 

【算法学习】

 

后缀数组能够在 nlogn的时间复杂度内求取出以下数组

 

【注意】明白字符串包含字符的范围 

 

SA [] 储存,第 i 个数字表示的是字典序第 i 大的后缀是以 SA_i 开始的后缀

即   “排第几的是哪个后缀

rank [] 储存,从第i位开始的后缀的字典序排名是 i

即   “某个后缀排第几个” 

 

对SA的求取,我们可以看作对所有的后缀进行排序

而这个排序显然如果直接莽的话肯定T,所以我们需要另外一种方法

 

这里使用基数排序+倍增的方法进行优化

 

将所有的后缀进行排序得到SA,这是我们的目的

 

基数排序,是对两个关键字的元素进行排序从而达到线性复杂度的方法

显然,在当两个后缀第一个字符相等的情况下,我们不可避免的去用第二个字符进行比较

这里我们需要注意到一个事情

第 i 个后缀的第二个字符是第 i + 1 的第一个字符

那这和直接莽好像没有什么区别?

 

 

所以我们就用到了倍增,每次确定后缀的以前 2 ^ k 长度的字符串排序的顺序

然后通过这个,就能够求出 2 ^ ( k + 1 ) 长度的后缀的前缀

通过,第 i 位的和第 i + k 位的,于是就能求出来

 

因为第一个字符有可能一样,导致有两个后缀排名相等

所以总排名数和后缀长度不同

所以长度为2时同理

而当所有总排名和后缀长度相同时,这个时候就找到了所有

 

下面是对使用到的各个数组的意义的描述:

c[] 桶,记录第 i 位的元素有多少个

x[] 后缀 i 的第一关键字,所以最开始是等于第 i 位字符

y[] 第二关键字排名第 i 的字符串,第一关键字的位置

 

【代码】

 首先求出所需要的几个数组的初值

//初始化
    int n = strlen(s+1);
    int m = 128;
       //m只需要大于 ascii(‘z')即可
      //因为第一步并不知道桶的规模
    for (int i = 1; i <= n; i++)
        ++c[x[i] = s[i]];
    //计算每一个字符的数量,同样的放在一起
    for (int i = 2; i <= m; i++)
        c[i] += c[i - 1];
    //求前缀和
    for (int i = n; i >= 1; i--)    
        SA[c[x[i]]--] = i;
    //从后往前,这样就能求出最开始顺序
    

倍增的同时进行排序

for (int k = 1; k <= n; k <<= 1)
//k是之前提到的,每次枚举的字符串长度
    {
        int num = 0;
        for (int i = n - k + 1; i <= n; i++)    
                             y[++num] = i;
                //先将最后的几个去掉
        for (int i = 1; i <= n; i++)
            if (SA[i] > k)
                y[++num] = SA[i] - k;
                //当这个位置的后缀在长度之外时
               //将这个位置的字符放到第二关键字中
        for (int i = 1; i <= m; i++)
            c[i] = 0;
                //清空桶
        for (int i = 1; i <= n; i++)
            c[x[i]]++;
                //放入第一关键字
                 //第一关键字是已经算好的
                 //第一次是初始化时计算好的
                //第二次的计算过程在下面
        for (int i = 2; i <= m; i++)
            c[i] += c[i - 1];
                //和初始化一样的求前缀和

        for (int i = n; i >= 1; i--)
            SA[c[x[y[i]]]--] = y[i], y[i] = 0;
                //在保证第一关键字的同时使用第二关键字排序
               //从后往前,第二关键字靠后的会被先剔除掉
               //考虑下数组的操作

        swap(x, y);
                //这里是为了保存上一步得到的y,没有太多其他意思
        x[SA[1]] = 1; num = 1;
        for (int i = 2; i <= n; i++)
            x[SA[i]] =
                  (y[SA[i]] == y[SA[i - 1]] && y[SA[i] + k] == y[SA[i - 1] + k]) ?
                  //这里注意回忆SA的意义
                   //比较的是和其排名相近的元素,也是最有可能两个关键字都相同的元素
                   //当字符的第二关键字,前后都相同时
                   //说明没有新的元素,所以不加
                   //如果有,则加
                   ? num 
                   : ++num;
        if (num == n) break;
        m = num;
    }          

 


 

【模板题】

 

【luogu P3809】

求一个字符串的SA

 

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 1000010;
char s[MAXN];
int  SA[MAXN];
int  x[MAXN], c[MAXN], y[MAXN];
void get_SA(char *s)
{
    //初始化
    int n = strlen(s + 1);
    int m = 128;
    for (int i = 1; i <= n; i++)
        ++c[x[i] = s[i]];
    //计算每一个字符的数量,同样的放在一起
    for (int i = 2; i <= m; i++)
        c[i] += c[i - 1];
    //求前缀和
    for (int i = n; i >= 1; i--)
        SA[c[x[i]]--] = i;
    //从后往前,这样就能求出最开始顺序

    for (int k = 1; k <= n; k <<= 1)
    {
        int num = 0;
        for (int i = n - k + 1; i <= n; i++)    y[++num] = i;
        //把最后几个字符放进去
        for (int i = 1; i <= n; i++)
            //按照顺序遍历
            if (SA[i] > k)
                //如果长度大于k
                y[++num] = SA[i] - k;
        //把第一个放进去
        for (int i = 1; i <= m; i++)
            c[i] = 0;
        for (int i = 1; i <= n; i++)
            c[x[i]]++;
        //x[i]是第一关键字
        for (int i = 2; i <= m; i++)
            c[i] += c[i - 1];
        //求前缀和
        for (int i = n; i >= 1; i--)
            SA[c[x[y[i]]]--] = y[i], y[i] = 0;
        swap(x, y);
        x[SA[1]] = 1; num = 1;
        for (int i = 2; i <= n; i++)
            x[SA[i]] = (y[SA[i]] == y[SA[i - 1]] && y[SA[i] + k] == y[SA[i - 1] + k]) ? num : ++num;
        if (num == n) break;
        m = num;
    }
    return;
}
int main()
{
    scanf("%s", s + 1);
    get_SA(s);
    int n = strlen(s + 1);
    for (int i = 1; i <= n; i++)
        printf("%d ", SA[i]);
    return 0;
}
View Code

 

 


【扩展】

 

LCP/height数组的求取

 

posted @ 2019-08-11 14:55  rentu  阅读(519)  评论(0编辑  收藏  举报