後綴數組學習筆記

近期學習了後綴數組。
以下是我個人對這一算法的理解。
後綴數組一共有兩種算法:倍增法和DC3算法。
前者可以實現 O(nlog2n) 而後者可以實現 O(n) 的時間複雜度來對一個字符串的每個後綴進行排序。
本文介紹的是後綴數組的倍增法,而DC3算法待填。
後綴數組是一種可以將一個字符串的後綴進行排序的算法。

後綴是什麼?
後綴是包含原字符串末尾字符的一個子串。
比如串 ababa ,它的所有後綴分別是:
ababa
baba
aba
ba
a
後綴數組可以將這些後綴按照字典序排序。
如上面的所有後綴按字典序從小到大排完序就爲:
a
aba
ababa
ba
baba
拍完序之後我們就可以再繼續做一些其他的操作了。
現在重點來了。我們需要怎樣才能排序呢?

我們先想:如果我們不會後綴數組呢?該怎麼辦?
1.  樸素的做法是我們寫一個 cmp 然後 sort 一下就好了。這樣做的複雜度是 O(n2log2n) 的,顯然很不優秀。
2.  我們考慮怎麼優化上面的做法,sort 的時間複雜度是沒辦法省去的,所以我們可以在 cmp 上做文章。我們可以用 hash + 二分 的方法來判斷兩個後綴的字典序, 這樣來排序的時間複雜度就是 O(nlog22n) 的。
雖然這個思考對後面對後綴數組的理解沒什麼幫助,但也提供了一個能讓我們在考場上寫暴力 / 騙分的優秀方法。

回到我們的主題,既然是倍增法的後綴數組,那麼我們是如何實現倍增來給後綴數組排序的?
簡要來說,倍增法的大致思想就是每次將長度爲 2x (1<2xn) 的已計算出排名的相鄰子串合併來計算合併後新子串的排名。
我們記合併前的前半部分的子串爲串 A , 後半部分的子串爲串 B , 合併後的子串爲串 C

在進行這個操作之前,我們要先處理處長度爲 1 的子串(每個字符)的排名。而每次合併之前,AB 的排名是已知的,那麼我們要如何求出 C 的排名呢?
我們發現, A 的排名 aB 的排名 b 是互不影響的,且 a 在字符串的比較中比 b 更加重要,因爲若兩個字符串的前半部分的完全相等,也就是說它們的 a 相等,我們纔會去比較它們 b 的大小來判斷它們字典序的大小關係。
所以我們就將 a 作爲第一關鍵字, b 作爲第二關鍵字來排序。

如果大家對關鍵字的概念不夠熟悉,看完上面的內容還有點懵,我們就來舉一個簡單的例子:我們給兩位數排序的時候就是以十位爲第一關鍵字,個位爲第二關鍵字來做的。

於是我們給 C 排序時,就可以將它看作一個特殊的兩位數 ab¯ ,只要對這個兩位數進行排序就可以了。

那麼我們用什麼排序方法來進行排序呢?
很明顯,這個兩位數的位數是非常少的,於是對於位數非常少的數進行排序,首先想到的一定是基數排序,這種排序方法可以在 O(n) 的時間複雜度內幫助我們完成這個排序。當然,在第一次對單個字符的排序中,基數排序只在字符集較小的情況下適用。若字符集較大,我們在第一次排序的過程中就選擇快速排序,這樣會更加優秀。

那麼具體該如何實現呢?
我們來看這張圖:
這裏寫圖片描述
圖源百度。
這張圖相信大家都見過了,但也許還並不瞭解這張圖的含義,下面是我自己對這張圖的理解。
圖中的第一行是原字符串的初始狀態。
以下每行中,若左邊爲的文本爲 rank ,右邊文本的長度爲 x ,則第 i 個數字表示從第 i 位開始的,長度爲 x 的字符串的排名。
若左邊的文本爲 x y 則表示在該次排序過程中,以每個 C 的第一關鍵字爲 x ,第二關鍵字爲 y 來進行排序。在該行內每個格子裏的兩個數字就是上文所說的 ab¯ 。我們對其進行排序得到下一次的 rank 。直到從第 i 位開始的字符串的 rank 都互不相同爲止。
我們發現,上圖中連接各行之間的有直線和斜線之分的。

若兩行之間只有直線就表示下一行是上一行進行排序後的新 rank 值。
若直線和斜線並存,直線和斜線共同連接下一行的位置即爲 C 的起始位置,直線連接上一行的位置爲 A 的起始位置,斜線連接的上一行的位置爲 B 的起始位置。

我們發現,在直線和斜線共存的兩行之間,有一些位置是沒有斜線的,這是爲什麼呢?後半部分沒有斜線的原因是在 C 串由兩個長度爲 2xAB 串拼接在一起時,B 串爲空串,所以 C 的第二關鍵字爲 0 ,不需討論。而前半部分是因爲若以該位置爲初始位置的長度爲 2x 的字符串要貢獻 B 串時,找不到完整的長度爲 2xA 串與它匹配,無法形成新的 C 串,所以對下一行的排序沒有影響。
當我們理解完這張圖之後,我們就可以來看具體代碼實現了:

我們以 UOJ#35 這道後綴數組模板題爲例:

讀入一個長度爲 n 的由小寫英文字母組成的字符串,請把這個字符串的所有非空後綴按字典序從小到大排序,然後按順序輸出後綴的第一個字符在原串中的位置。位置編號爲 1n
除此之外爲了進一步證明你確實有給後綴排序的超能力,請另外輸出 n1 個整數分別表
示排序後相鄰後綴的最長公共前綴的長度。

先來看給後綴排序的這一部分,這一部分用後綴數組實現就可以了:
代碼中的 n 爲字符串的長度, 初始的 m 爲字符集大小。

for(R int i = 1; i <= m; i++) ws[i] = 0;
for(R int i = 1; i <= n; i++) ws[x[i] = Str[i] - 'a' + 1]++;
for(R int i = 2; i <= m; i++) ws[i] += ws[i - 1];
for(R int i = n; i > 0; i--) sa[ws[ x[i] ]--] = i;

這一步是實現了給字符串中的單個字符排序的操作。
在排序的過程中順便完成了原字符串由字符向數字的轉換。
排序原理見計數排序(做一輪的基數排序)。

for(R int j = 1, p; j <= n && p < n; j <<= 1, m = p)

這一行枚舉了當前每個串 A , B 長度均爲 2x=j ,不同排名個數爲 p 時對字符串的排序過程。
很明顯我們知道,當前長度 j 一定要 n ,而當不同排名個數等於 n 時,我們就已經完成對後綴的排序了,每次我們將 j 翻倍, m 變爲當前不同排名的個數。因爲我們需要排序的一,二關鍵字是字符串的排名,所以 m 就只要開到字符串當前不同排名的個數就好了。

在排序的過程中,我們把字符串的上一輪第一關鍵字也就是字符串的排名記在 x 數組中,把作爲第二關鍵字的字符串從大到小記在 y 數組中。
注意了,我們爲什麼可以直接把作爲第二關鍵字的字符串按照排名記在 yi 中呢?
這裏就要用到後綴數組的一個性質了。

我們觀察上面的圖,然後可以發現,若當前 AB 長度爲 j ,則從 nj+1 開始的 C 的第二關鍵字都爲 0 ,將這種情況下 A 的起始位置直接丟進數組,就像這樣:
(其中因爲 p 暫時沒有作用,我們把它作爲一個 tmp 來使用)。

p = 0;
for(R int i = n - j + 1; i <= n; i++) y[++p] = i;

然後我們發現,只有位置 j+1 的字符串纔會作爲 B 貢獻進這一次排序中的 C 裏。所以我們按照上一次排序後的排名從小到大枚舉,若當前排名的字符串的位置符合條件,則將與它匹配的 A 的起始位置加入 y 數組, 而 y 依然滿足排好序的條件,且結合上面那步, y 中的元素依然是 n 個。實際操作是這樣:

for(R int i = 1; i <= m; i++)
    if(sa[i] > j) y[++p] = sa[i];

然後我們要完成的就是對第一關鍵字的排序了,具體操作和第一步很像,只是將上面的 xi 變爲了 xyi ,這樣表示將第二關鍵字排名爲 yi 的字符串按照第一關鍵字進行排序,代碼如下:

for(R int i = 1; i <= m; i++) ws[i] = 0;
for(R int i = 1; i <= n; i++) ws[ x[ y[i] ] ]++;
for(R int i = 2; i <= m; i++) ws[i] += ws[i - 1];
for(R int i = 1; i <= n; i++) sa[ws[ x[ y[i] ] ]--] = y[i];

然後我們就是要計算合併之後的 Crank 值了,這個值我們是要存在 x 數組裏的,然而我們在計算時需要用到上一次的 x 值,所以我們就可以用到現在暫時沒有用的 y 數組,將原本的 x 數組裏的排名放入 y 數組即可,這個操作可以用交換指針來很好的完成。
然後計算當前的 rank 時,如果基數排序後的兩個數的排名相鄰,我們就只需要比較它們的第一關鍵字和第二關鍵字就可以知道它們是否完全相等了。
代碼如下:

//cmp
bool cmp(R int *s, R int a, R int b, R int l)
{
    return a + l <= n && b + l <= n && s[a] == s[b] && s[a + l] == s[b + l];
}
//code
R int *t;
t = x, x = y, y = t;
x[ sa[1] ] = p = 1;
for(R int i = 2; i <= n; i++)
    x[ sa[i] ] = cmp(y, sa[i - 1], sa[i], j) ? p : ++p; 

所以計算排名爲 i 的代碼如下:

char Str[Maxn];
int ws[Maxn], wa[Maxn], wb[Maxn], sa[Maxn], rank[Maxn], height[Maxn];
bool cmp(R int *s, R int a, R int b, R int l)
{
    return a + l <= n && b + l <= n && s[a] == s[b] && s[a + l] == s[b + l];
}
void SA()
{
    R int *t, *x = wa, *y = wb;
    for(R int i = 1; i <= m; i++) ws[i] = 0;
    for(R int i = 1; i <= n; i++) ws[x[i] = Str[i] - 'a' + 1]++;
    for(R int i = 2; i <= m; i++) ws[i] += ws[i - 1];
    for(R int i = n; i > 0; i--) sa[ws[ x[i] ]--] = i;
    for(R int j = 1, p = 0; p < n && j <= n; j <<= 1, m = p)
    {
        p = 0;
        for(R int i = n - j + 1; i <= n; i++) y[++p] = i;
        for(R int i = 1; i <= n; i++) 
            if(sa[i] > j) y[++p] = sa[i] - j;
        for(R int i = 1; i <= m; i++) ws[i] = 0;
        for(R int i = 1; i <= n; i++) ws[ x[ y[i] ] ]++;
        for(R int i = 2; i <= m; i++) ws[i] += ws[i - 1];
        for(R int i = n; i > 0; i--) sa[ws[ x[ y[i] ] ]--] = y[i];
        t = x, x = y, y = t;
        p = 1, x[ sa[1] ] = 1;
        for(R int i = 2; i <= n; i++)
            x[ sa[i] ] = cmp(y, sa[i - 1], sa[i], j) ? p : ++p;
    }
    return ;
}

以上就是對字符串中的後綴排序的過程。
那麼該如何求出相鄰後綴的 LCP 呢?
//施工中先貼模板
後綴數組模板(UOJ#35):

#include <algorithm>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <queue>
#include <set>
#include <stack>

#define R register
#define ll long long
#define db double
#define sqr(_x) (_x) * (_x)
#define Cmax(_a, _b) ((_a) < (_b) ? (_a) = (_b), 1 : 0)
#define Cmin(_a, _b) ((_a) > (_b) ? (_a) = (_b), 1 : 0)
#define Max(_a, _b) ((_a) > (_b) ? (_a) : (_b))
#define Min(_a, _b) ((_a) < (_b) ? (_a) : (_b))
#define Abs(_x) (_x < 0 ? (-(_x)) : (_x))

using namespace std;

namespace Dntcry
{
    inline int read()
    {
        R int a = 0, b = 1; R char c = getchar();
        for(; c < '0' || c > '9'; c = getchar()) (c == '-') ? b = -1 : 0;
        for(; c >= '0' && c <= '9'; c = getchar()) a = (a << 1) + (a << 3) + c - '0';
        return a * b;
    }
    inline ll lread()
    {
        R ll a = 0, b = 1; R char c = getchar();
        for(; c < '0' || c > '9'; c = getchar()) (c == '-') ? b = -1 : 0;
        for(; c >= '0' && c <= '9'; c = getchar()) a = (a << 1) + (a << 3) + c - '0';
        return a * b;
    }
    const int Maxn = 100010;
    int n, m;
    char Str[Maxn];
    int ws[Maxn], wa[Maxn], wb[Maxn], sa[Maxn], rank[Maxn], height[Maxn];
    bool cmp(R int *s, R int a, R int b, R int l)
    {
        return a + l <= n && b + l <= n && s[a] == s[b] && s[a + l] == s[b + l];
    }
    void SA()
    {
        R int *t, *x = wa, *y = wb;
        for(R int i = 1; i <= m; i++) ws[i] = 0;
        for(R int i = 1; i <= n; i++) ws[x[i] = Str[i] - 'a' + 1]++;
        for(R int i = 2; i <= m; i++) ws[i] += ws[i - 1];
        for(R int i = n; i > 0; i--) sa[ws[ x[i] ]--] = i;
        for(R int j = 1, p = 0; p < n && j <= n; j <<= 1, m = p)
        {
            p = 0;
            for(R int i = n - j + 1; i <= n; i++) y[++p] = i;
            for(R int i = 1; i <= n; i++) 
                if(sa[i] > j) y[++p] = sa[i] - j;
            for(R int i = 1; i <= m; i++) ws[i] = 0;
            for(R int i = 1; i <= n; i++) ws[ x[ y[i] ] ]++;
            for(R int i = 2; i <= m; i++) ws[i] += ws[i - 1];
            for(R int i = n; i > 0; i--) sa[ws[ x[ y[i] ] ]--] = y[i];
            t = x, x = y, y = t;
            p = 1, x[ sa[1] ] = 1;
            for(R int i = 2; i <= n; i++)
                x[ sa[i] ] = cmp(y, sa[i - 1], sa[i], j) ? p : ++p;
        }
        return ;
    }
    void Celheight()
    {
        for(R int i = 1; i <= n; i++) rank[ sa[i] ] = i;
        for(R int i = 1, k = 0, j; i <= n; height[ rank[i++] ] = k) if(rank[i] > 1)
        {
            k ? k-- : 0;
            for(j = sa[rank[i] - 1]; i + k <= n && j + k <= n && Str[i + k] == Str[j + k]; k++);
        }
        return ;
    }
    int Main()
    {   
        scanf("%s", Str + 1);
        m = 30;
        n = strlen(Str + 1);
        SA();
        Celheight();
        for(R int i = 1; i <= n; i++) printf("%d ", sa[i]); putchar('\n');
        for(R int i = 2; i <= n; i++) printf("%d ", height[i]); putchar('\n');
        return 0;
    }
}
int main()
{
    return Dntcry :: Main();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章