(轉)後綴數組講解

目錄

 


回到頂部

什麼是後綴數組

後綴數組是處理字符串的有力工具 —羅穗騫

個人理解:後綴數組是讓人蒙逼的有力工具!

就像上面那位大神所說的,後綴數組可以解決很多關於字符串的問題,

譬如這道題

 

注意:後綴數組並不是一種算法,而是一種思想。

實現它的方法主要有兩種:倍增法O(nlogn)O(nlogn) 和 DC3法O(n)O(n)

其中倍增法除了僅僅在時間複雜度上不佔優勢之外,其他的方面例如編程難度,空間複雜度,常數等都秒殺DC3法

 

我的建議:深入理解倍增法,並能熟練運用(起碼8分鐘內寫出來&&沒有錯誤)。DC3法只做瞭解,吸取其中的精髓;

 

但是由於本人太辣雞啦,所以本文只討論倍增法

 

回到頂部

前置知識

後綴

這個大家應該都懂吧。。

比如說aabaaaabaabaaaab

它的後綴爲

基數排序

我下面會詳細講

現在,你可以簡單的理解爲

基數排序在後綴數組中可以在O(n)O(n)的時間內對一個二元組(p,q)(p,q)進行排序,其中pp是第一關鍵字,qq是第二關鍵字

比其他的排序算法都要優越

回到頂部

倍增法

首先定義一坨變量

sa[i]sa[i]:排名爲ii的後綴的位置

rak[i]rak[i]:從第ii個位置開始的後綴的排名,下文爲了敘述方便,把從第ii個位置開始的後綴簡稱爲後綴ii

tp[i]tp[i]:基數排序的第二關鍵字,意義與sasa一樣,即第二關鍵字排名爲ii的後綴的位置

tax[i]tax[i]:ii號元素出現了多少次。輔助基數排序

ss:字符串,s[i]s[i]表示字符串中第ii個字符串

 

可能大家覺得sasa和rakrak這兩個數組比較繞,沒關係,多琢磨一下就好

事實上,也正是因爲這樣,才使得兩個數組可以在O(n)O(n)的時間內互相推出來

具體一點

rak[sa[i]]=irak[sa[i]]=i

sa[rak[i]]=isa[rak[i]]=i

 

那我們怎麼對所有的後綴進行排序呢?

我們把每個後綴分開來看。

開始時,每個後綴的第一個字母的大小是能確定的,也就是他本身的asciiascii值

具體點?把第ii個字母看做是(s[i],i)(s[i],i)的二元組,對其進行基數排序。這樣我們可以保證asciiascii小的在前面,若asciiascii相同則先出現的在前面

 

這樣我們就得到了他們的在完成第一個字母的排序之後的相對位置關係

 

接下來呢?

不要忘了, 我們算法的名稱叫做“倍增法”,每次將排序長度*2,最多需要log(n)log(n)次便可以完成排序

因此我們現在需要對每個後綴的前兩個字母進行排序

 

此時第一個字母的相對關係我們已經知道了。

那第二個字母的大小呢?我們還需要一次排序麼?

其實大可不必,因爲我們忽略了一個非常重要的性質:第ii個後綴的第二個字母,實際是第i+1i+1個後綴的第一個字母

 

因此每個後綴的第二個字母的相對位置關係我們也是知道的。

我們用tptp這個數組把他記錄出來,對(rak,tp)(rak,tp)這個二元組進行基數排序

tp[i]tp[i]表示的是第二關鍵字中排名爲ii的後綴的位置,rakrak表示的是上一輪中第ii個後綴的排名。

對於一個長度爲ww的後綴,你可以形象的理解爲:第一關鍵字針對前w2w2個字符形成的字符串,第二關鍵字針對後w2w2個字符形成的字符串

 

接下來我們需要對每個後綴的前四個字母組成的字符串進行排序

此時我們已經知道了每個後綴前兩個字母的排名,而第ii個後綴的第3,43,4個字母恰好是第i+2i+2個後綴的前兩個字母。

他們的相對位置我們又知道啦。

 

這樣不斷排下去,最後就可以完成排序啦

 

我相信大家看到這裏肯定是一臉mengbi

下面我結合代碼和具體的排序過程給大家演示一下

 

回到頂部

過程詳解

按照上面說的,開始時rakrak爲字符的ascii碼,第二關鍵字爲它們的相對位置關係

這裏的aa數組是字符串數組

然後我們對其進行排序,我們暫且先不管它是如何進行排序,因爲排序的過程非常難理解,一會兒我重點講一下。

 

各個數組的大小

 

然後我們進行倍增。

 

這裏再定義幾個變量

MM:字符集的大小,基數排序時會用到。不理解也沒關係

pp:排名的多少(有幾個不同的後綴)

注意在排序的過程中,各個後綴的排名可能是相同的。因爲我們在倍增的過程中只是對其前幾個字符進行排名。

但是,對於每個後綴來說,最終的排名一定是不同的!畢竟每個後綴的長度都不相同

 

下面是倍增的過程

ww表示倍增的長度,當各個排名都不相同時,我們便可以退出循環。

M=pM=p是對基數排序的優化,因爲字符集大小就是排名的個數

 

 

這兩句話是對第二關鍵字進行排序

假設我們現在需要得到的長度爲ww,那麼sa[i]sa[i]表示的實際是長度爲w2w2的後綴中排名爲ii的位置(也就是上一輪的結果)

我們需要得到的tp[i]tp[i]表示的是:長度爲ww的後綴中,第二關鍵字排名爲ii的位置。

之所以能這樣更新,是因爲ii號後綴的前w2w2個字符形成的字符串是i−w2i−w2號後綴的後w2w2個字符形成的字符串

算了直接上圖吧,。。

(注意此圖的邊界與代碼中有區別,原因是代碼中的ww表示我們已經得到了長度爲ww的結果,現在正要去更新長度爲2w2w的結果)

 

 

此時的pp並不是統計排名的個數,只是一個簡單的計數器

注意:有一些後綴是沒有第二關鍵字的,他們的第二關鍵字排名排名應該在最前面。

 

此時第一二關鍵字都已經處理好了,我們進行排序

排完序之後,我們得到了一個新的sasa數組

此時我們用sasa數組來更新rakrak數組

 

我們前面說過rakrak數組是可能會重複的,所以我們此時用pp來表示到底出現了幾個名次

還需要注意一個事情,在判斷是否重複的時候,我們需要用到上一輪的rakrak

而此時tptp數組是沒有用的,所以我們直接交換tptp和rakrak

當然你也可以寫爲

 

 

在判斷重複的時候,我們實際上是對一個二元組進行比較。

 

當滿足判斷條件時,兩個後綴的名次一定是相同的(想一想,爲什麼?)

 

 然後愉快的輸出就可以啦!

 

放一下代碼

 

複製代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
const int MAXN = 1e6 + 10;
using namespace std;
char s[MAXN];
int N, M, rak[MAXN], sa[MAXN], tax[MAXN], tp[MAXN];
void Debug() {
    printf("*****************\n");
    printf("下標"); for (int i = 1; i <= N; i++) printf("%d ", i);     printf("\n");
    printf("sa  "); for (int i = 1; i <= N; i++) printf("%d ", sa[i]); printf("\n");
    printf("rak "); for (int i = 1; i <= N; i++) printf("%d ", rak[i]); printf("\n");
    printf("tp  "); for (int i = 1; i <= N; i++) printf("%d ", tp[i]); printf("\n");
}
void Qsort() {
    for (int i = 0; i <= M; i++) tax[i] = 0;
    for (int i = 1; i <= N; i++) tax[rak[i]]++;
    for (int i = 1; i <= M; i++) tax[i] += tax[i - 1];
    for (int i = N; i >= 1; i--) sa[ tax[rak[tp[i]]]-- ] = tp[i];
    //這部分我的文章的末尾詳細的說明了
}
void SuffixSort() {
    M = 75;
    for (int i = 1; i <= N; i++) rak[i] = s[i] - '0' + 1, tp[i] = i;
    Qsort();
    Debug();
    for (int w = 1, p = 0; p < N; M = p, w <<= 1) {
        //w:當前倍增的長度,w = x表示已經求出了長度爲x的後綴的排名,現在要更新長度爲2x的後綴的排名
        //p表示不同的後綴的個數,很顯然原字符串的後綴都是不同的,因此p = N時可以退出循環
        p = 0;//這裏的p僅僅是一個計數器000

        
        for (int i = 1; i <= w; i++) tp[++p] = N - w + i;  // 給末尾的w個字符寫入第二關鍵字排序,因爲它們沒第二關鍵字,所以默認爲排名最靠前
        // 下面for的i表示從小到大遍歷第二關鍵字的排序,i表示排名
        for (int i = 1; i <= N; i++) if (sa[i] > w) tp[++p] = sa[i] - w; //這兩句是後綴數組的核心部分,我已經畫圖說明
        Qsort();//此時我們已經更新出了第二關鍵字,利用上一輪的rak更新本輪的sa
        std::swap(tp, rak);//這裏原本tp已經沒有用了
        rak[sa[1]] = p = 1;
        for (int i = 2; i <= N; i++)
            // 理論上rak[sa[i]] = i,但是因爲可能有重複排名,所以需要變通一下
            // 需要判斷排名i和i-1的是否第一和第二關鍵字是否排序相同
            rak[sa[i]] = (tp[sa[i - 1]] == tp[sa[i]] && tp[sa[i - 1] + w] == tp[sa[i] + w]) ? p : ++p;
        //這裏當兩個後綴上一輪排名相同時本輪也相同,至於爲什麼大家可以思考一下
        Debug();
    }
    for (int i = 1; i <= N; i++)
        printf("%d ", sa[i]);
}
int main() {
    scanf("%s", s + 1);
    N = strlen(s + 1);
    SuffixSort();
    return 0;
}

複製代碼

 

 

 

 

再補一下調試結果

 

回到頂部

基數排序

如果你對上面的主體過程有了大致的瞭解,那麼基數排序的過程就不難理解了

在閱讀下面內容之前,我希望大家能初步瞭解一下基數排序

https://baike.baidu.com/item/%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F/7875498?fr=aladdin

大致看一下它給出的例子和c++代碼就好

 

 

先來大致看一下,代碼就44行

 

 

MM:字符集的大小,一共需要多少個桶

taxtax:元素出現的次數,在這裏就是名次出現的次數

 

第一行:把桶清零

第二行:統計每個名詞出現的次數

第三行:做個前綴和(啪,廢話)

可能大家會疑惑前綴和有什麼用?

利用前綴和可以快速的定位出每個位置應有的排名

具體的來說,前綴和可以統計比當前名次小的後綴有多少個。

第四行:@#¥%……&*

我知道大家肯定看暈了,我們先來回顧一下這幾個數組的定義

這裏我們假設已經得到了ww長度的排名,要更新2w2w長度的排名

sa[i]sa[i]:長度爲ww的後綴中,排名爲ii的後綴的位置

rak[i]rak[i]:長度爲ww的後綴中,從第ii個位置開始的後綴的排名

tp[i]tp[i]:長度爲2w2w的後綴中,第二關鍵字排名爲ii的後綴的位置

我們考慮如果把串長爲ww擴展爲2w2w會有哪些變化

首先第一關鍵字的相對位置是不會改變的,唯一有變化的是rakrak值相同的那些後綴,我們需要根據tptp的值來確定他們的相對位置

煮個栗子,rakrak相同,tp[1]=2,tp[2]=4tp[1]=2,tp[2]=4,那麼從44開始的後綴排名比從22開始的後綴排名靠後

再回來看這句話應該就好明白了

首先我們倒着枚舉ii,

那麼sa[tax[rak[tp[i]]]−−]sa[tax[rak[tp[i]]]−−]的意思就是說:

我從大到小枚舉第二關鍵字,再用rak[i]rak[i]定位到第一關鍵字的大小

那麼tax[rak[tp[i]]]tax[rak[tp[i]]]就表示當第一關鍵字相同時,第二關鍵字較大的這個後綴的排名是啥

得到了排名,我們也就能更新sasa了

 

回到頂部

height數組

個人感覺,上面說的一大堆,都是爲heightheight數組做鋪墊的,heightheight數組纔是後綴數組的精髓、

先說定義

ii號後綴:從ii開始的後綴

lcp(x,y)lcp(x,y):字符串xx與字符串yy的最長公共前綴,在這裏指xx號後綴與與yy號後綴的最長公共前綴

height[i]height[i]:lcp(sa[i],sa[i−1])lcp(sa[i],sa[i−1]),即排名爲ii的後綴與排名爲i−1i−1的後綴的最長公共前綴

H[i]H[i]:height[rak[i]]height[rak[i]],即ii號後綴與它前一名的後綴的最長公共前綴

 

性質:H[i]⩾H[i−1]−1H[i]⩾H[i−1]−1

證明引自遠航之曲大佬

 

update in 2019.3.28

在複習的時候我發現這裏的證明有一個跳點,包括論文中的證明也有一點不嚴謹的地方

下面兩處畫紅線的地方均沒有證明"suffix(k+1)"與"i前一名的後綴之間的關係",實際上這兩者之間的關係是:他們的lcp至少爲h[i - 1] - 1。可以用反證法證明,在此不再贅述

 

能夠線性計算height[]的值的關鍵在於h[](height[rank[]])的性質,即h[i]>=h[i-1]-1,下面具體分析一下這個不等式的由來。

我們先把要證什麼放在這:對於第i個後綴,設j=sa[rank[i] – 1],也就是說j是i的按排名來的上一個字符串,按定義來i和j的最長公共前綴就是height[rank[i]],我們現在就是想知道height[rank[i]]至少是多少,而我們要證明的就是至少是height[rank[i-1]]-1。

好啦,現在開始證吧。

首先我們不妨設第i-1個字符串(這裏以及後面指的“第?個字符串”不是按字典序排名來的,是按照首字符在字符串中的位置來的)按字典序排名來的前面的那個字符串是第k個字符串,注意k不一定是i-2,因爲第k個字符串是按字典序排名來的i-1前面那個,並不是指在原字符串中位置在i-1前面的那個第i-2個字符串。

這時,依據height[]的定義,第k個字符串和第i-1個字符串的公共前綴自然是height[rank[i-1]],現在先討論一下第k+1個字符串和第i個字符串的關係。

第一種情況,第k個字符串和第i-1個字符串的首字符不同,那麼第k+1個字符串的排名既可能在i的前面,也可能在i的後面,但沒有關係,因爲height[rank[i-1]]就是0了呀,那麼無論height[rank[i]]是多少都會有height[rank[i]]>=height[rank[i-1]]-1,也就是h[i]>=h[i-1]-1。

第二種情況,第k個字符串和第i-1個字符串的首字符相同,那麼由於第k+1個字符串就是第k個字符串去掉首字符得到的,第i個字符串也是第i-1個字符串去掉首字符得到的,那麼顯然第k+1個字符串要排在第i個字符串前面,要麼就產生矛盾了。同時,第k個字符串和第i-1個字符串的最長公共前綴是height[rank[i-1]],那麼自然第k+1個字符串和第i個字符串的最長公共前綴就是height[rank[i-1]]-1。

到此爲止,第二種情況的證明還沒有完,我們可以試想一下,對於比第i個字符串的字典序排名更靠前的那些字符串,誰和第i個字符串的相似度最高(這裏說的相似度是指最長公共前綴的長度)?顯然是排名緊鄰第i個字符串的那個字符串了呀,即sa[rank[i]-1]。也就是說sa[rank[i]]和sa[rank[i]-1]的最長公共前綴至少是height[rank[i-1]]-1,那麼就有height[rank[i]]>=height[rank[i-1]]-1,也即h[i]>=h[i-1]-1。

 

 

代碼

複製代碼

void GetHeight() {
    int j, k = 0;
    for(int i = 1; i <= N; i++) {
        if(k) k--;
        int j = sa[rak[i] - 1];
        while(s[i + k] == s[j + k]) k++;
        Height[rak[i]] = k;
        printf("%d\n", k);
    }
}

複製代碼

 

 

回到頂部

經典應用

兩個後綴的最大公共前綴

lcp(x,y)=min(heigh[x−y])lcp(x,y)=min(heigh[x−y]), 用rmq維護,O(1)查詢

可重疊最長重複子串

Height數組裏的最大值

不可重疊最長重複子串 POJ1743

首先二分答案xx,對height數組進行分組,保證每一組的minheightminheight都>=x>=x

依次枚舉每一組,記錄下最大和最小長度,多sa[mx]−sa[mi]>=xsa[mx]−sa[mi]>=x那麼可以更新答案

本質不同的子串的數量

枚舉每一個後綴,第ii個後綴對答案的貢獻爲len−sa[i]+1−height[i]len−sa[i]+1−height[i]

回到頂部

後記

本蒟蒻也是第一次看這麼難的東西。

第一次見這種東西應該是去年夏天吧,那時我記得自己在機房裏瞅着這幾行代碼看了一晚上也沒看出啥來。

現在再來看也是死磕了一天多才看懂。

不過我還是比較好奇。

這種東西是誰發明的啊啊啊啊啊腦洞也太大了吧啊啊啊啊啊啊

哦對了,後綴數組還有一個非常有用的數組叫做heightheight,這個數組更神奇,,有空再講吧。 已補充

 

作者:自爲風月馬前卒

個人博客http://attack204.com//

出處:http://zwfymqz.cnblogs.com/

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

標籤: 後綴數組

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章