五分鐘搞懂字尾陣列!字尾陣列解析以及應用(附詳解程式碼)
為什麼學字尾陣列
字尾陣列是一個比較強大的處理字串的演算法,是有關字串的基礎演算法,所以必須掌握。
學會字尾自動機(SAM)就不用學字尾陣列(SA)了?不,雖然SAM看起來更為強大和全面,但是有些SAM解決不了的問題能被SA解決,只掌握SAM是遠遠不夠的。
……
有什麼SAM做不了的例子?
比如果求一個串字尾的lcp方面的應用,這是SA可以很方便的用rmq來維護,但是SAM還要求lca,比較麻煩,還有就是字符集比較大的時候SA也有優勢。
現在這裡放道題,看完這個blog可能就會做了!:
你可想想這道題:你有一個01串S,然後定義一個字首最右邊的位置就是這個字首的結束位置。現在有q多個詢問,每個詢問結束位置在l~r中不同字首的最長公共字尾是多長?
時限4s
而下面是我對字尾陣列的一些理解
構造字尾陣列——SA
先定義一些變數的含義
Str :需要處理的字串(長度為Len)
Suffix[i] :Str下標為i ~ Len的連續子串(即字尾)
Rank[i] : Suffix[i]在所有後綴中的排名
SA[i] : 滿足Suffix[SA[1]] < Suffix[SA[2]] …… < Suffix[SA[Len]],即排名為i的字尾為Suffix[SA[i]] (與Rank是互逆運算)
好,來形象的理解一下
字尾陣列指的就是這個SA[i],有了它,我們就可以實現一些很強大的功能(如不相同子串個數、連續重複子串等)。如何快速的到它,便成為了這個演算法的關鍵
現在比較主流的演算法有兩種,倍增和DC3,在這裡,就主要講一下稍微慢一些,但比較好實現以及理解的倍增演算法(雖說慢,但也是O(Len logLen))的。
進入正題——倍增演算法
倍增演算法的主要思想 :對於一個字尾Suffix[i],如果想直接得到Rank比較困難,但是我們可以對每個字元開始的長度為
這樣做有什麼好處呢?
設每一輪得到的序列為rank(注意r
為什麼呢?為了方便描述,設SubStr(i, len)為從第i個字元開始,長度為len的字串我們可以把第k輪SubStr(i,
我們用下面這張經典的圖理解一下:
相信只要理解字串的比較法則(跟實數差不多),理解起來並不難。#還有一個細節就是怎麼把這些兩位數排序?這種位數少的數進行排序毫無疑問的要用一個複雜度為長度*排序數的個數的優美演算法——基數排序(對於兩位數的數複雜度就是O(Len)的)。
基數排序原理 : 把數字依次按照由低位到高位依次排序,排序時只看當前位。對於每一位排序時,因為上一位已經是有序的,所以這一位相等或符合大小條件時就不用交換位置,如果不符合大小條件就交換,實現可以用”桶”來做。(敘說起來比較奇怪,看完下面的程式碼應該更好理解,也可以上網查有關資料)
好了SA和Rank(大寫R)到此為止就處理好了。(下面有詳解程式碼!)。但我們發現,只有這兩樣東西好像沒什麼用,為了處理重複子串之類的問題,我們就要引入一個表示最長公共字首的新助手Height陣列!
構造最長公共字首——Height
同樣先是定義一些變數
Heigth[i] : 表示Suffix[SA[i]]和Suffix[SA[i - 1]]的最長公共字首,也就是排名相鄰的兩個字尾的最長公共字首
H[i] : 等於Height[Rank[i]],也就是字尾Suffix[i]和它前一名的字尾的最長公共字首
而兩個排名不相鄰的最長公共字首定義為排名在它們之間的Height的最小值。
跟上面一樣,先形像的理解一下:
高效地得到Height陣列
如果一個一個數按SA中的順序比較的話複雜度是O(
H[i] ≥ H[i - 1] - 1!
如果上面這個性質是對的,那我們可以按照H[1]、H[2]……H[Len]的順序進行計算,那麼複雜度就降為O(N)了!
讓我們嘗試一下證明這個性質 : 設Suffix[k]是排在Suffix[i - 1]前一名的字尾,則它們的最長公共字首是H[i - 1]。都去掉第一個字元,就變成Suffix[k + 1]和Suffix[i]。如果H[i - 1] = 0或1,那麼H[i] ≥ 0顯然成立。否則,H[i] ≥ H[i - 1] - 1(去掉了原來的第一個,其他字首一樣相等),所以Suffix[i]和在它前一名的字尾的最長公共字首至少是H[i - 1] - 1。
仔細想想還是比較好理解的。H求出來,那Height就相應的求出來了,這樣結合SA,Rank和Height我們就可以做很多關於字串的題了!
程式碼——Code
建議複製到自己的程式設計軟體上看
/*
Problem: JZOJ1598(詢問一個字串中有多少至少出現兩次的子串)
Content: SA's Code and Explanation
Author : YxuanwKeith
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
char ch[MAXN], All[MAXN];
int SA[MAXN], rank[MAXN], Height[MAXN], tax[MAXN], tp[MAXN], a[MAXN], n, m;
char str[MAXN];
//rank[i] 第i個字尾的排名; SA[i] 排名為i的字尾位置; Height[i] 排名為i的字尾與排名為(i-1)的字尾的LCP
//tax[i] 計數排序輔助陣列; tp[i] rank的輔助陣列(計數排序中的第二關鍵字),與SA意義一樣。
//a為原串
void RSort() {
//rank第一關鍵字,tp第二關鍵字。
for (int i = 0; i <= m; i ++) tax[i] = 0;
for (int i = 1; i <= n; i ++) tax[rank[tp[i]]] ++;
for (int i = 1; i <= m; i ++) tax[i] += tax[i-1];
for (int i = n; i >= 1; i --) SA[tax[rank[tp[i]]] --] = tp[i]; //確保滿足第一關鍵字的同時,再滿足第二關鍵字的要求
} //計數排序,把新的二元組排序。
int cmp(int *f, int x, int y, int w) { return f[x] == f[y] && f[x + w] == f[y + w]; }
//通過二元組兩個下標的比較,確定兩個子串是否相同
void Suffix() {
//SA
for (int i = 1; i <= n; i ++) rank[i] = a[i], tp[i] = i;
m = 127 ,RSort(); //一開始是以單個字元為單位,所以(m = 127)
for (int w = 1, p = 1, i; p < n; w += w, m = p) { //把子串長度翻倍,更新rank
//w 當前一個子串的長度; m 當前離散後的排名種類數
//當前的tp(第二關鍵字)可直接由上一次的SA的得到
for (p = 0, i = n - w + 1; i <= n; i ++) tp[++ p] = i; //長度越界,第二關鍵字為0
for (i = 1; i <= n; i ++) if (SA[i] > w) tp[++ p] = SA[i] - w;
//更新SA值,並用tp暫時存下上一輪的rank(用於cmp比較)
RSort(), swap(rank, tp), rank[SA[1]] = p = 1;
//用已經完成的SA來更新與它互逆的rank,並離散rank
for (i = 2; i <= n; i ++) rank[SA[i]] = cmp(tp, SA[i], SA[i - 1], w) ? p : ++ p;
}
//離散:把相等的字串的rank設為相同。
//LCP
int j, k = 0;
for(int i = 1; i <= n; Height[rank[i ++]] = k)
for( k = k ? k - 1 : k, j = SA[rank[i] - 1]; a[i + k] == a[j + k]; ++ k);
//這個知道原理後就比較好理解程式
}
void Init() {
scanf("%s", str);
n = strlen(str);
for (int i = 0; i < n; i ++) a[i + 1] = str[i];
}
int main() {
Init();
Suffix();
int ans = Height[2];
for (int i = 3; i <= n; i ++) ans += max(Height[i] - Height[i - 1], 0);
printf("%d\n", ans);
}
4個比較基礎的應用
Q1:一個串中兩個串的最大公共字首是多少?
A1:這不就是Height嗎?用rmq預處理,再O(1)查詢。
Q2:一個串中可重疊的重複最長子串是多長?
A2:就是求任意兩個字尾的最長公共字首,而任意兩個字尾的最長公共字首都是Height 數組裡某一段的最小值,那最長的就是Height中的最大值。
Q3:一個串種不可重疊的重複最長子串是多長?
A3:先二分答案,轉化成判別式的問題比較好處理。假設當前需要判別長度為k是否符合要求,只需把排序後的字尾分成若干組,其中每組的字尾之間的Height 值都不小於k,再判斷其中有沒有不重複的字尾,具體就是看最大的SA值和最小的SA值相差超不超過k,有一組超過的話k就是合法答案。
A4:一個字串不相等的子串的個數是多少?
Q4:每個子串一定是某個字尾的字首,那麼原問題等價於求所有後綴之間的不相同的字首的個數。而且可以發現每一個字尾Suffix[SA[i]]的貢獻是Len - SA[i] + 1,但是有子串算重複,重複的就是Heigh[i]個與前面相同的字首,那麼減去就可以了。最後,一個字尾Suffix[SA[i]]的貢獻就是Len - SA[k] + 1 - Height[k]。
對於字尾陣列更多的應用這裡就不詳細闡述,經過思考後每個人都會發現它的一些不同的用途,它的功能也許比你想象中的更強大!
最開始的那道題
先搬下來。。。
你可想想這道題:你有一個01串S,然後定義一個字首最右邊的位置就是這個字首的結束位置。現在有很多個詢問,每q個詢問結束位置在l~r中不同字首的最長公共字尾是多長?
時限4s
簡單思路:首先可以把字串反過來就是求字尾的最長公共字首了,可以用SA求出height陣列,然後用rmq預處理之後就是求兩個位置間的最小值。然後對於一個區間,顯然只有在SA陣列中相鄰的兩個串可以貢獻答案。
對於區間詢問的問題可以用莫隊處理,然後考慮加入一個字尾應該怎麼處理,我們可以維護一個按SA陣列排序的連結串列。假設我們先把所有位置的SA全部加入,然後按順序刪除,重新按順序加入時就可以O(1)完成修改。那麼按照這個思路我們可以用固定左端點的並查集,做到只加入,不刪除,然後用
*可能後面的處理方式比較麻煩,如果直接用splay維護區間中的字尾的話可以做到
結束
以上就是我對字尾陣列的理解 ——YxuanwKeith