後綴數組學習小結(已經死了)

一直想學習後綴數組,但是lrj的算法經典上只給出了原理和代碼,代碼上面沒有註釋,讓人很難讀懂。所以我對後綴數組的瞭解一直停留在知道這個東西和倍增法的原理,至於板子和套路完全不知。
最近還是死啃了板子,把我現在掌握的東西記成博文,也方便自己和大家。
本人也不是很會這個,如果dalao們發現了問題,請指正!

首先,倍增法在算法競賽上已經講得非常詳細了,在此我也不再贅述,大家自己閱讀書吧。我在這就把lrj給出的代碼註釋一下,順便改正一下他在寫的裏面的錯誤。

這是劉汝佳給出的代碼(注意,在註釋!!!!!的地方,書上的i 的起始標記寫錯了,應該是1而不是0)

void build_sa(int m) {
    int *x = t, *y = t2;
    for (int i = 0; i < m; i ++) c[i] = 0;
    for (int i = 0; i < n; i ++) c[x[i] = s[i]] ++;
    for (int i = 1; i < m; i ++) c[i] += c[i - 1];
    for (int i = n - 1; i >= 0; i --) sa[-- c[x[i]]] = i;
    for (int k = 1; k < n; k <<= 1) {
        int p = 0;
        for (int i = n - k; i < n; i ++) y[p ++] = i;
        for (int i = 0; i < n; i ++) if (sa[i] >= k) y[p ++] = sa[i] - k;
        for (int i = 0; i < m; i ++) c[i] = 0;
        for (int i = 0; i < n; i ++) c[x[y[i]]] ++;
        for (int i = 1; i < m; i ++) c[i] += c[i - 1];  //!!!!!
        for (int i = n - 1; i >= 0; i --) sa[-- c[x[y[i]]]] = y[i];
        swap(x, y);
        p = 1; x[sa[0]] = 0;
        for (int i = 1; i < n; i ++)
            x[sa[i]] = (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k]) ? p - 1 : p ++;
        if (p >= n) break;
        m = p;
    }
}

如果是第一次看這個代碼,相信你也我一開始一樣一臉懵逼,下來我細細講解一下各個數組的含義。

首先,倍增法需要開4倍的輔助空間,其中有結果數組sa,臨時數組t,t2,計數數組c。
在具體的函數中,劉汝佳用x和y分別指代了t和t2。

下面是講解每個數組的具體含義:
假設原串s,s的長度爲n。
sa[i],指的是在s中的所有後綴中,字典序第i小的後綴在原串中的起始位置下標。0<=i<n,0<=sa[i]<n
t和t2只是臨時的數組,沒有十分大的意義;
但是在函數中,x和y數組有具體的意義
x[i],指在倍增的過程中,當前計算出來的第i位的第一關鍵字的字符值。注意,隨着算法的進展,x[i]一直會發生變化。第一關鍵字允許出現重複。
y[i],指在倍增的過程中,當前計算出來的第i小的第二關鍵字在s中的所在位置。注意,隨着算法的進展,y[i]一直會發生變化。第i小和第i + 1小的第二關鍵字的大小允許相同,但是仍需要給定不同的位次。
c[i],指倍增過程中,第一關鍵字的字符值小於等於i的個數。

解釋了一下各個數組的含義,我們開始講解函數每一步的實際含義。

char tmp[maxn];
int s[maxn];
int rank_sa[maxn], height[maxn];
int sa[maxn], t[maxn], t2[maxn], c[maxn], n;


// 以字符值數組s構造sa,字符值從0-m-1
void build_sa(int m) {                                             //字符值最大值
    int *x = t, *y = t2;
    for (int i = 0; i < m; i ++) c[i] = 0;                         //清空計數數組
    for (int i = 0; i < n; i ++) c[x[i] = s[i]] ++;                //統計一下每個第一關鍵字的個數
    for (int i = 1; i < m; i ++) c[i] += c[i - 1];                 //計算第一關鍵字個數的前綴和
    for (int i = n - 1; i >= 0; i --) sa[-- c[x[i]]] = i;          //粗配後綴數組  因爲對於一個第一關鍵字x的後綴,最多有c[x] - 1個後綴在前,那就把當前的後綴認爲是第c[x] - 1(從0開始)個好了;同時,i遞減代表了第一字符值大小遞減,而c[x]是根據x遞增的
    for (int k = 1; k < n; k <<= 1) {                              //倍增長度
        int p = 0;                                                 //p作爲排序順序,也作爲數組下標
        for (int i = n - k; i < n; i ++) y[p ++] = i;              //因爲對於一個固定的k,後面的k個的第二關鍵字是0(看書),所以第二關鍵字一定排在前k個
        for (int i = 0; i < n; i ++) if (sa[i] >= k) y[p ++] = sa[i] - k; //對於剩下的,第sa[i] - k個的第二關鍵字就是第sa[i]個的第一關鍵字,所以按照sa的大小(記住sa是從小到大的)順序給剩下的n-k個付上第二關鍵字,賦值的先後決定了y的大小排列
        for (int i = 0; i < m; i ++) c[i] = 0;                     //下四行同上。只是i變成了y[i],因爲不僅需要第一關鍵字排序,也需要按照第二關鍵字排列。
        for (int i = 0; i < n; i ++) c[x[y[i]]] ++;                
        for (int i = 1; i < m; i ++) c[i] += c[i - 1];
        for (int i = n - 1; i >= 0; i --) sa[-- c[x[y[i]]]] = y[i];
        swap(x, y);                                                //交換x,y;  現在的y變成了先前的x數組,現在的x數組暫時失去了意義。
        p = 1; x[sa[0]] = 0;
        for (int i = 1; i < n; i ++)                               //p也被重新附上了新的意義,重新標第一關鍵字的大小。現在的第一關鍵字需要根據之前的第一與第二關鍵字字典序排序標號。
            x[sa[i]] = (y[sa[i - 1]] == y[sa[i]] && y[sa[i - 1] + k] == y[sa[i] + k]) ? p - 1 : p ++; //y[sa[i - 1]] == y[sa[i]],第一關鍵字相同,y[sa[i - 1] + k] == y[sa[i] + k]第二關鍵字相同,同時相同標相同的號,否則號遞增。因爲sa數組是從小到大的,所以順序根據sa確定了
        if (p >= n) break;
        m = p;                                                     //現在的字符值變成了p個
    }
}

這樣我感覺應該講清楚了。。
還是需要自己花一定的時間去理解,有一個大致的概念,應該能夠方便理解代碼的含義吧。
總結一下後綴數組的倍增法,這裏面的關係挺繞的,也挺複雜的。有很多變量有雙重的身份,我覺得這樣十分巧妙,非常佩服。

只求出了一個字符串的後綴數組沒有任何意義,下來就是體現後綴數組意義的地方了。

A、多模式串匹配
處理了原串的後綴數組,然後就能二分查詢了

int m;
int cmp_suffix(char *pat, int p) {
    return strncmp(pat, s + sa[p], m);
}

int find_st(char *P) {
    m = strlen(P);
    if (cmp_suffix(P, 0) < 0) return -1;            //比最小的小或比最大的大都不行
    if (cmp_suffix(P, n - 1) > 0) return -1;
    int lb = 0, ub = n - 1;
    while (lb <= ub) {                              //後綴隨sa遞增,可以二分
        int mid = lb + (lb + ub) >> 1;
        int res = cmp_suffix(P, mid);
        if (!res) return mid;
        if (res < 0) ub = mid - 1;
        else lb = mid + 1;
    }
    return -1;
}

B、求公共子串
需要用到最長公共前綴。我覺得白書上解釋的挺詳細的,應該挺好懂,就不贅述了。
白書上的會數組越界,現在已經改正

void getHeight(int n) {
    int k = 0;
    height[0] =  0;  
    for (int i = 0; i < n; i ++) rank_sa[sa[i]] = i;
    for(int i = 0; i < n - 1; i ++) {  
        int j = sa[rank_sa[i] - 1];  
        while(i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
        height[rank_sa[i]] = k;
        k = max(0, k - 1);  
    }  
}

求公共子串的套路就是把所有的給的串合成一個大字符串,每個小串間用一個不存在的字符隔開,然後求sa,求height,按要求找需要的值就行了。

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