後綴數組詳解

什麼是後綴數組

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

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

就像上面那位大神所說的,後綴數組能夠解決不少關於字符串的問題,算法

譬如這道題編程

 

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

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

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

 

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

 

可是因爲本人太辣雞啦,因此本文只討論倍增法3d

 

前置知識

後綴

這個你們應該都懂吧。。調試

好比說$aabaaaab$

它的後綴爲

基數排序

我下面會詳細講

如今,你能夠簡單的理解爲

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

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

倍增法

首先定義一坨變量

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

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

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

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

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

 

可能你們以爲$sa$和$rak$這兩個數組比較繞,不要緊,多琢磨一下就好

事實上,也正是由於這樣,才使得兩個數組能夠在$O(n)$的時間內互相推出來

具體一點

$rak[sa[i]]=i$

$sa[rak[i]]=i$

 

那咱們怎麼對全部的後綴進行排序呢?

咱們把每一個後綴分開來看。

開始時,每一個後綴的第一個字母的大小是能肯定的,也就是他自己的$ascii$值

具體點?把第$i$個字母看作是$(s[i],i)$的二元組,對其進行基數排序。這樣咱們能夠保證$ascii$小的在前面,若$ascii$相同則先出現的在前面

 

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

 

接下來呢?

不要忘了, 咱們算法的名稱叫作「倍增法」,每次將排序長度*2,最多須要$log(n)$次即可以完成排序

所以咱們如今須要對每一個後綴的前兩個字母進行排序

 

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

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

其實大可沒必要,由於咱們忽略了一個很是重要的性質:第$i$個後綴的第二個字母,實際是第$i+1$個後綴的第一個字母

 

所以每一個後綴的第二個字母的相對位置關係咱們也是知道的。

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

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

對於一個長度爲$w$的後綴,你能夠形象的理解爲:第一關鍵字針對前$\frac{w}{2}$個字符造成的字符串,第二關鍵字針對後$\frac{w}{2}$個字符造成的字符串

 

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

此時咱們已經知道了每一個後綴前兩個字母的排名,而第$i$個後綴的第$3,4$個字母剛好是第$i+2$個後綴的前兩個字母。

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

 

這樣不斷排下去,最後就能夠完成排序啦

 

我相信你們看到這裏確定是一臉mengbi

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

 

過程詳解

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

這裏的$a$數組是字符串數組

而後咱們對其進行排序,咱們暫且先無論它是如何進行排序,由於排序的過程很是難理解,一下子我重點講一下。

 

各個數組的大小

 

而後咱們進行倍增。

 

這裏再定義幾個變量

$M$:字符集的大小,基數排序時會用到。不理解也不要緊

$p$:排名的多少(有幾個不一樣的後綴)

注意在排序的過程當中,各個後綴的排名多是相同的。由於咱們在倍增的過程當中只是對其前幾個字符進行排名。

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

 

下面是倍增的過程

$w$表示倍增的長度,當各個排名都不相同時,咱們即可以退出循環。

$M=p$是對基數排序的優化,由於字符集大小就是排名的個數

 

 

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

假設咱們如今須要獲得的長度爲$w$,那麼$sa[i]$表示的實際是長度爲$\frac{w}{2}$的後綴中排名爲$i$的位置(也就是上一輪的結果)

咱們須要獲得的$tp[i]$表示的是:長度爲$w$的後綴中,第二關鍵字排名爲$i$的位置。

之因此能這樣更新,是由於$i$號後綴的前$\frac{w}{2}$個字符造成的字符串是$i - \frac{w}{2}$號後綴的後$\frac{w}{2}$個字符造成的字符串

算了直接上圖吧,。。

(注意此圖的邊界與代碼中有區別,緣由是代碼中的$w$表示咱們已經獲得了長度爲$w$的結果,如今正要去更新長度爲$2w$的結果)

 

 

此時的$p$並非統計排名的個數,只是一個簡單的計數器

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

 

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

排完序以後,咱們獲得了一個新的$sa$數組

此時咱們用$sa$數組來更新$rak$數組

 

咱們前面說過$rak$數組是可能會重複的,因此咱們此時用$p$來表示到底出現了幾個名次

還須要注意一個事情,在判斷是否重複的時候,咱們須要用到上一輪的$rak$

而此時$tp$數組是沒有用的,因此咱們直接交換$tp$和$rak$

固然你也能夠寫爲

 

 

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

 

當知足判斷條件時,兩個後綴的名次必定是相同的(想想,爲何?)

 

 而後愉快的輸出就能夠啦!

 

放一下代碼

 

#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;
        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]] = (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++代碼就好

 

 

先來大體看一下,代碼就$4$行

 

 

$M$:字符集的大小,一共須要多少個桶

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

 

第一行:把桶清零

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

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

可能你們會疑惑前綴和有什麼用?

利用前綴和能夠快速的定位出每一個位置應有的排名

具體的來講,前綴和能夠統計比當前名次小的後綴有多少個。

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

我知道你們確定看暈了,咱們先來回顧一下這幾個數組的定義

這裏咱們假設已經獲得了$w$長度的排名,要更新$2w$長度的排名

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

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

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

咱們考慮若是把串長爲$w$擴展爲$2w$會有哪些變化

首先第一關鍵字的相對位置是不會改變的,惟一有變化的是$rak$值相同的那些後綴,咱們須要根據$tp$的值來肯定他們的相對位置

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

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

首先咱們倒着枚舉$i$,

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

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

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

獲得了排名,咱們也就能更新$sa$了

 

height數組

我的感受,上面說的一大堆,都是爲$height$數組作鋪墊的,$height$數組纔是後綴數組的精髓、

先說定義

$i$號後綴:從$i$開始的後綴

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

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

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

 

性質:$H[i] \geqslant 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])$, 用rmq維護,O(1)查詢

可重疊最長重複子串

Height數組裏的最大值

不可重疊最長重複子串 POJ1743

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

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

本質不一樣的子串的數量

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

後記

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

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

如今再來看也是死磕了一天多才看懂。

不過我仍是比較好奇。

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

哦對了,後綴數組還有一個很是有用的數組叫作$height$,這個數組更神奇,,有空再講吧。 已補充

相關文章
相關標籤/搜索