後綴數組入門

後綴數組的定義:

後綴數組 (Suffix Array) 指某個字符串的全部後綴字典排序後獲得的數組。數組中只保存後綴開始的位置。簡稱SA。html

後綴:從某個字符串的某個開始位置到其末尾的字符串子串,包括原串和空字符串。

例子:{ABC}的後綴{ABC},{BC},{C},{}c++

字典排序: 默認從小到大git

構造後綴數組:

  • 樸素作法:將n個字符串進行sort排序,時間複雜度\(O(n^2log_2n)\)算法

  • 倍增數組法: Manber和Myers發明,須要進行 \(log_2n\) 次排序,排序時間複雜度 \(O(nlog_2n)\) ,因此總時間複雜度是 \(O(nlog_2^2n)\) ,能夠用基數排序將sort排序進行優化,總時間複雜度優化成 \(O(nlog_2n)\)數組

  • 因此通常來講,倍增數組的方法夠用了,更快的能夠去找[SA-IS 算法]或者[DC3算法]優化

倍增數組法的代碼:

步驟:

  1. 開始以長度爲1的後綴字符串爲排序規則對其SA進行排序,並求出其排名Rank(藍色區域)
  2. 以長度k的後綴字符串的Rank爲排序規則對SA排序,求出長度爲2k的後綴字符串的排序結果.並求出長度爲2k的Rank.緣由:長度爲2k的後綴字符串的rank總能由長度爲k的後綴字符串的rank求出(綠色區域->綠色區域)
  3. 重複步驟2 直到構造出完整的SA。

(圖:以以前的Rank構造新的Rank的過程)
ui

下面給出用未優化後的代碼。編碼

未優化代碼(點擊展開)

#include <cstdio>
#include <cstring>
#include <algorithm>
#define MAXN 1000
using namespace std;

char str[MAXN];//字符串數組
int sa[MAXN + 1];//後綴數組,+1是爲了存儲(空字符串)
int rank[MAXN + 1];//Rank[i]第i位開始的子串排名(0~N)
int tmp[MAXN+1];
int k,n;

bool cmp_sa(const int &i,const int &j) {
    if(rank[i] != rank[j])  return rank[i]<rank[j];
    else {
    /**由先前的rank求出如今的rank,
    好比{AB} 要由{A}和{B}的rank一塊兒求出,由於{A}和{B}是連在一塊兒而且{AB}的長度是{B}的2k倍,因此加上長度k就能夠求出{B}的rank**/
        int l = i+k<=n?rank[i+k]:-1;
        int r = j+k<=n?rank[j+k]:-1;
        return l<r;
    }
}

void build_sa(const char* str,int *sa) {
    n = strlen(str);
    //長度爲1的sa,rank取編碼,由於空字符串排最前,這裏取-1
    for(int i=0; i<=n; i++) {
        sa[i] = i;
        rank[i] = rank[i] < n? str[i]:-1;
    }
    //倍增思想
    for(k=1; k<n; k*=2) {
        //對sa進行排序,也是對長度爲2*k的後綴字符串進行排序
        sort(sa,sa+n+1,cmp_sa);
        tmp[sa[0]] = 0;
        for(int i=1; i<=n; i++) {//計算新的rank
            tmp[sa[i]] = tmp[sa[i-1]] + (cmp_sa(sa[i-1],sa[i])?1:0);
        }
        for(int i=0; i<=n; i++) {
            rank[i] = tmp[i];
        }
    }
}

int main() {
    scanf("%s",&str);
    build_sa(str,sa);
    return 0;
}

優化:

能夠看出影響咱們的算法複雜度的主要因素是字符串的排序算法。爲了優化咱們的算法,咱們得選擇一些更快的排序方法。爲了與網上大多數後綴數組模板統一,這裏咱們選擇LSD基數排序,也就是表中的低位字符串排序做爲替代品。spa

先介紹幾個概念設計

基數排序:從各級關鍵字的最低有效位開始依次進行穩定排序(計數排序,桶排序等等具備穩定性的排序)。因爲可能存在多級關鍵字,因此基數排序分爲LSD(least significant digit)和MSD(most significant digit)

計數排序能夠做爲基數排序的一個子過程。

以二位整數位爲例子,介紹下LSD基數排序,由於每位數只有0~9這10個數字,那麼咱們須要構造10個桶(0~9),而後開始選取關鍵字,那就是各位上的數字,根據數的個位數上的數,十位數的數,將二位整數依次放入0~9的桶中。

圖:序列{11,83,81,21,63} 經過LSD基數排序變爲 {11,21,63,83}

這只是LSD基數排序的原理而已。

要將基數排序運用到字符串上還須要一些小改變。具體能夠參考《算法》第四版的 5.1 字符串排序章節。

優化代碼(點擊展開)

//參考代碼
//https://codeforces.com/contest/1037/submission/42406942
//https://codeforces.com/contest/1037/submission/42965008
#include <cstdio>
#include <string>
#include <algorithm>
#include <cctype>
#include <cstring>

#define MAXN 1000010
#define MAXM 127

using namespace std;

char str[MAXN]; //字符串數組
int c[MAXN]; //計數排序的桶
int RA[MAXN],tempRA[MAXN];
int SA[MAXN],tempSA[MAXN];

void count_sort(int k,int n,int m) {
    for(int i=0; i<m; i++) c[i]=0;
    //第一次循環,ABCAB字符串中,最後一個字符沒有第二部分,因此優先級最高 
    for(int i=0; i<n; i++) ++c[i+k<n?RA[i+k]:0];
    for(int i=1; i<m; i++) c[i]+=c[i-1];
    //根據以前的結果求出新的Sa數組 
    for(int i=n-1; i>=0; i--) tempSA[--c[SA[i]+k<n?RA[SA[i]+k]:0]]=SA[i];
    for(int i=0; i<n; i++) SA[i] = tempSA[i];
}

void get_sa(const char* str) {//m表示排序字符串最大ASCII值,也就是桶最大容量 
    int n=strlen(str),m,q;
    m = max(MAXM,n);
    for(int i=0; i<n; i++) RA[i]=str[i]; //初始化Rank
    for(int i=0; i<n; i++) SA[i] = i;
    //進行屢次基數排序
    for(int k=1; k<n; k<<=1) {
        //根據{AB}字符串的{B}部分進行SA進行計數排序,在根據{A}部分進行一次.
        //基數排序原理:年月日的話,就各對日,月,年進行一次穩定排序.
        count_sort(k,n,m);
        count_sort(0,n,m);
        //計算Rank
        tempRA[SA[0]] = q = 0;
        //用舊的Rank算出新的Rank(倍增原理)
        //第一次循環 ABCAB => 
        //      Rank{02301}
        for(int i=1; i<n; i++) tempRA[SA[i]] = (RA[SA[i]]==RA[SA[i-1]]&&RA[SA[i]+k]==RA[SA[i-1]+k]?q:++q);
        for(int i=0; i<n; i++) RA[i] = tempRA[i];
        if(q==n-1) break;//優化,每一個桶的元素個數 <= 1,就不用繼續排序了
        m=q+1;//優化,減小桶的數量
    }
    for(int i=0;i<n;i++) printf("%d ",SA[i]+1);
}

int main() {
    scanf("%s",&str);
    get_sa(str);
    return 0;
}

//abracadabra


未註釋壓縮核心代碼

#include <cstdio>
#include <string>
#include <algorithm>
#include <cctype>
#include <cstring>
#define MAXN 1000010
#define MAXM 127

using namespace std;

char str[MAXN];
int c[MAXN];
int RA[MAXN],tempRA[MAXN];
int SA[MAXN],tempSA[MAXN];

void count_sort(int k,int n,int m) {
    for(int i=0; i<m; i++) c[i]=0;
    for(int i=0; i<n; i++) ++c[i+k<n?RA[i+k]:0];
    for(int i=1; i<m; i++) c[i]+=c[i-1];
    for(int i=n-1; i>=0; i--) tempSA[--c[SA[i]+k<n?RA[SA[i]+k]:0]]=SA[i];
    for(int i=0; i<n; i++) SA[i] = tempSA[i];
}

inline void get_sa(const char* str) {
    int n=strlen(str),m;
    m = max(MAXM,n);
    for(int i=0; i<n; i++) RA[SA[i] = i]=str[i];
    for(int k=1,q; k<n; k<<=1,m=q+1) {
        count_sort(k,n,m);
        count_sort(0,n,m);
        tempRA[SA[0]] = q = 0;
        for(int i=1; i<n; i++) tempRA[SA[i]] = (RA[SA[i]]==RA[SA[i-1]]&&RA[SA[i]+k]==RA[SA[i-1]+k]?q:++q);
        for(int i=0; i<n; i++) RA[i] = tempRA[i];
        if (q == n - 1) break;
    }
    for(int i=0; i<n; i++) printf("%d ",SA[i]+1);
}

int main() {
    scanf("%s",str);
    get_sa(str);
    return 0;
}


應用:

字符串匹配:

求字符串T在字符串S中出現的位置,經過二分搜索就能夠在 \(O(|T|log_2|S|)\) 時間內完成,適合 \(|S|\) 字符串較長的狀況。

int contain(string S,string T){
    int l=0,r=S.length()-1;
    while(l<=r){
        int mid = (r+l)/2;
        //以sa[mid]爲下標開始,長度爲T.length()的字符串S與字符串T比較的結果
        int re = S.compare(sa[mid],T.length(),T);
        if(re<0) l=mid+1;
        else if(re>0)r=mid-1;
        else return sa[mid];
    }
    return -1;
}


高度數組(LCP Array,Longest Common Prefix Array):

(PS:等我補完多校再說)


題目:

參考博客和文獻:

http://www.javashuo.com/article/p-gigkycdp-cn.html
http://www.javashuo.com/article/p-qzopzpmz-cc.html

挑戰程序設計競賽(第2版)《算法》第四版《算法導論》

如下兩篇我的強烈推薦。

  1. https://cp-algorithms.com/string/suffix-array.html
  2. https://www.geeksforgeeks.org/suffix-array-set-2-a-nlognlogn-algorithm/
相關文章
相關標籤/搜索