后缀数组讲解

后缀数组零基础讲解(倍增法)

最近几天好好复习了下SA,希望看完这篇博客能让你彻底明白SA(求height什么的有待填坑)

(偷偷告诉你们:其实学SA有个很简单的方法,那就是背下来!)

    1.后缀数组的应用和优势(可以先看如何实现再看):

   应用:后缀数组可以得到后缀的排名,如果将后缀排序后,则后缀是按照字典序排列的,那么就可以很好地求出任意两个后缀的最长公共前缀了。

   假设排序后的后缀中第i个后缀为s'[i],如果s'[i]和s'[i-1]的最长公共前缀长度为A,s'[i-1]和s'[i-2]最长公共前缀长度为B,则s'[i]和s'[i-2]最长公共前缀长度为min(A,B)

   下面用反证法证明:

   如果s'[i]和s'[i-2]的最长公共前缀长度大于min(A,B)

   则因为按照字典序排序,则可以知道s'[i-1]必定也含有该公共前缀,长度>min(A,B),与假设不符

   证毕

   据此可以得出任意两个后缀的最长公共前缀,设height[i]为排名为i的后缀与排名为i-1的后缀的最长公共前缀,则排名为L的后缀排名为R的后缀的最长公共前缀长度为min(height[i])(i=L+1,L+2...,R)预处理+RMQ可以解决(如果真的零基础可以先放着看下面)

   后缀数组不仅能求一个子串的后缀排序,还能搞多个子串,只要每个子串后加个不会出现在子串的字符即可,这样就可以实现很多功能了。

   优势:后缀数组和SAM比有什么优势呢?SAM可以实现的东西很多,但SA不行,那SA还有什么学的必要吗?

   这不废话吗,那你为什么再看我这个讲SA的博客(doge)

   SA一般作为学SAM的过渡,SA学好了也能解决大部分问题,学的特别好甚至能乱搞一切SAM题(如果不是请告诉我qwq)

   SA占用空间比SAM小很多,而且代码难度上来说也要小一些,理解难度也要小很多。

    2.后缀数组的原理,实现

   倍增法

   假设我们已经得到了每个后缀按照前2x个字符排序的排名,则可以得出每个后缀按照前2x+1个字符排序的排名(第x次可以得出按前2x-1个字符比较的排名,因为第一次是比较1个),

   令s'[i]表示开头位置为i的后缀,rk[i]表示s'[i]的排名(每次排序后更新,第i次排序时用第i-1次的rk,第i次排序后更新rk)

   则如果我得到了第x次的rk,现在要比较s'[i]和s'[j]长度为2x+1的前缀的大小,那么我可以先比较rk[i]和rk[j],如果rk[i]和rk[j]不同可以直接得出大小关系,如果相同则继续比较rk[i+(1<<(x-1))]和rk[j+(1<<(x-1))]即将一个长度为2x+1的比较分为两次长度为2x的比较。

   这样便能实现快速比较了,但是要如何排序呢?

   这里就需要用到桶排序

   桶排序简单来说就是桶是按照顺序排列的,我要比较若干个数的话,就将这些数丢进对应编号的桶里面,然后按顺序从桶里取出来,那么这些数就是按照顺序排好的了。

   举个栗子:3、1、4、5、3要排序,我可以设个数组a[N](N视数字最大值定,也可以用离散化减小N取值),然后a[3]++,a[1]++,a[4]++,a[5]++,a[3]++

   再从1到N一次将桶里的数取出,如果桶里没有数不取,如果有多个取出多个。

   当然桶排序也可以有多个关键字,就是会略微麻烦些,例如我们需要比较两次,并且优先第一次。

   接下来是重头戏,请务必看清楚各个数组表示的意思!(注:所有数组指的都是当前的,即第x次比较则都为所有后缀长度为2x-1的前缀,需要每次排序后更新)

   sa[i]表示排名为i的位置

   rk[i]表示开头位置为i的后缀的排名

   tp[i]表示第二关键字排名为i的位置

   tax为桶数组,用于桶排序

   下面上代码(请自行将代码拷贝仔细看清每一步!)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int rk[N], tp[N];
int sa[N], tax[N], m;
char s[N];
int n;
inline void qsort() {//桶排序 
    memset(tax + 1, 0, m << 2);//初始化 
    for (int i = 1; i <= n; ++i) ++tax[rk[i]];//根据第一关键字丢到桶里 
    for (int i = 1; i <= m; ++i) tax[i] += tax[i - 1];//求前缀和,因为还需要用第二关键字
    //tax[i]即为所有rk<=i的数目,tax[i-1]即为所有rk<=i-1的数
    //因此求出的sa数组中第tax[i-1]+1个到第tax[i]个即为rk=i的 
    for (int i = n; i; --i) sa[tax[rk[tp[i]]]--] = tp[i];
    //这个需要好好理解,首先rk如果相同则所在的桶是相同的,那么我们直接根据第二关键字顺序将其取出即可
    //tp[i]为第二关键字排名为i的位置
    //假设tax[i-1]=A,tax[i]=B,则说明第i个桶有B-A个,说明有B-A个第一关键字即rk=i的,
    //此时我们如果先取出第二关键字最靠后的位置,那么sa[B]就是这个位置
    //之后取出第二关键字次大的,那么sa[B-1]就是这个位置,依次类推 
}
inline void suffix_sort() {
    m = 200;//m为辅助用,用于记录当前的rk最大为多少,因为当两个后缀长度为2^i的前缀相同时rk算作相同
    //是一个优化 
    for (int i = 1; i <= n; ++i) rk[i] = s[i], tp[i] = i;//初始第一关键字即为Ascall码大小,也可以自定义大小
    //例如s只包含小写字母,rk[i]=s[i]-'a'+1即可,此时m可取26
    //tp为第二关键字,当不存在时根据位置确定第二关键字 
    qsort();
    for (int i = 1, p; p < n; m = p, i <<= 1) {//i即为上一次比较长度,i*2为这一次比较长度 
        p = 0;
        for (int j = 1; j <= i; ++j) tp[++p] = n - i + j;//因为上一次比较长度为i
        //后面开头位置为n-i+1,n-i+1...n的第二关键字都不存在,此时将位置作为第二关键字
        //又因为不存在要优先考虑,因此放在存在第二关键字的前面 
        for (int j = 1; j <= n; ++j)
            if (sa[j] > i)
                tp[++p] = sa[j] - i;//上一次的sa可以求出这一次的第二关键字
        //因为这里sa[j]表示上一次排名为j的位置
        //而tp第二关键字实际上就是上一次排名为j的位置-i,因为第一关键字和第二关键字就是开头位置相隔i而已 
        qsort(), swap(rk, tp);//桶排序后,交换下rk,tp,因为接下来还需要用到上一次的rk以及求出这一次的rk 
        rk[sa[1]] = p = 1;
        for (int j = 2; j <= n; ++j)
            rk[sa[j]] = (tp[sa[j]] == tp[sa[j - 1]] && tp[sa[j] + i] == tp[sa[j - 1] + i]) ? p : ++p;
        //此时的tp因为和上一次的rk交换,所以表示的是上一次的rk 
        //如果上一次的时候第一关键字和第二关键字j和j-1相同,则这一次第一关键字j与j-1相同 
    }
}
int main() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    suffix_sort();
    for (int i = 1; i <= n; ++i) printf("%d ", sa[i]); 
    return 0;
}

求height数组有待填坑!

SA相关题型有待填坑!

posted @ 2020-08-29 17:00  wjr5082  阅读(95)  评论(0)    收藏  举报