【字尾陣列】解決各種字串問題的總結
一、 求字尾陣列
對DA(倍增演算法)的一些個人理解:由於我只學習了倍增演算法,所以我只能談談我對它的理解。DC3演算法我沒有去研究....
DA演算法我是根據羅穗騫的模板寫的,根據自己的理解做了些許的小優化。我們現在來看看羅穗騫大牛的模板:
int wa[maxn],wb[maxn],wv[maxn],ws[maxn];
int cmp(int *r,int a,int b,int l)
{return r[a]==r[b]&&r[a+l]==r[b+l];}
void da(int *r,int *sa,int n,int m)
{
int i,j,p,*x=wa,*y=wb,*t;
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[x[i]=r[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;
for(j=1,p=1;p<n;j*=2,m=p)
{
for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;
for(i=0;i<n;i++) wv[i]=x[y[i]];
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[wv[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
}
return;
}
其實,我個人認為,對於這個演算法以及程式碼,無需過分深入地理解,只需記憶即可,理解只是為了幫助記憶罷了。先解釋變數:n為字串長度,m為字元的取值範圍,r為字串。後面的j為每次排序時子串的長度。
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[x[i]=r[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[x[i]]]=i;
這四行程式碼,進行的是對R中長度為1的子串進行基數排序。x陣列在後面需要用到,所以先複製r陣列的值。特別需要注意的是,第四行的for
for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;
這兩行程式碼,利用了上一次基數排序的結果,對待排序的子串的第二關鍵字進行了一次高效地基數排序。我們可以結合下面的圖來理解:
不難發現,除了第一次基數排序以外,之後的每次雙關鍵字排序,設此次排序子串長度為j,則從第n-j位開始的子串,其第二關鍵字均為0,所以得到第一個for語句:for(p=0,i=n-j;i<n;i++) y[p++]=i;
for(i=0;i<n;i++) wv[i]=x[y[i]];
for(i=0;i<m;i++) ws[i]=0;
for(i=0;i<n;i++) ws[wv[i]]++;
for(i=1;i<m;i++) ws[i]+=ws[i-1];
for(i=n-1;i>=0;i--) sa[--ws[wv[i]]]=y[i];
與一開始的4個for語句意義相同,基數排序。至於為什麼wv[i]=x[y[i]],這個我想了蠻久沒想通...硬記算了- -哪位朋友理解的希望能告訴我一聲...
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
這個for語句中的初始化語句裡,完成了x陣列和y陣列的交換,用了指標的交換節約時間,簡化程式碼。這裡需要注意的是p和i的初始值都是1,不是0.其實如果記得後面的語句,不難看出它們的初始值不能為0,因為後面有i-1和p-1嘛。這個for語句的意義要結合cmp函式來理解。反正,你知道這裡p的值表示的是此時關鍵字不同的串的數量就對了。當p=n的時候,說明所有串都已經排好序了(它們的排名都唯一確定)。所以,一開始的迴圈語句中,迴圈條件是(p<n)。
另外,在使用倍增演算法前,需要保證r陣列的值均大於0。然後要在原字串後新增一個0號字元,具體原因參見羅穗騫的論文。這時候,若原串的長度為n,則實際要進行字尾陣列構建的r陣列的長度應該為n+1.所以呼叫da函式時,對應的n應為n+1.
二、字尾陣列的延伸--height陣列
在介紹字尾陣列的應用前,先介紹字尾陣列的一個重要附屬陣列:height陣列。
1、height 陣列:定義height[i]=suffix(sa[i-1])和suffix(sa[i])的最長公共字首,也就是排名相鄰的兩個字尾的最長公共字首。
height陣列是應用字尾陣列解題是的核心,基本上使用字尾陣列解決的題目都是依賴height陣列完成的。
2、height陣列的求法:具體的求法參見羅穗騫的論文。對於height陣列的求法,我並沒有去深刻理解,單純地記憶了而已...有興趣的朋友可以去鑽研鑽研再和我交流交流
這裡給出程式碼:
int rank[maxn],height[maxn];
void calheight(int *r,int *sa,int n)
{
int i,j,k=0;
for(i=1;i<=n;i++) rank[sa[i]]=i;
for(i=0;i<n;height[rank[i++]]=k)
for(k?k--:0,j=sa[rank[i]-1];r[i+k]==r[j+k];k++);
return;
}
3、一些注意事項:height陣列的值應該是從height[1]開始的,而且height[1]應該是等於0的。原因是,因為我們在字串後面添加了一個0號字元,所以它必然是最小的一個字尾。而字串中的其他字元都應該是大於0的(前面有提到,使用倍增演算法前需要確保這點),所以排名第二的字串和0號字元的公共字首(即height[1])應當為0.在呼叫calheight函式時,要注意height陣列的範圍應該是[1..n]。所以呼叫時應該是calheight(r,sa,n)而不是calheight(r,sa,n+1)。要理解清楚這裡的n的含義是什麼。
calheight過程中,對rank陣列求值的for語句的初始語句是i=1而不是i=0的原因,和上面說的類似,因為sa[0]總是等於那個已經失去作用的0號字元,所以沒必要求出其rank值。當然你錯寫成for (i=0..),也不會有什麼問題。
三、字尾陣列解題總結:
1、求單個子串的不重複子串個數。SPOJ 694、SPOJ 705.
這個問題是一個特殊求值問題。要認識到這樣一個事實:一個字串中的所有子串都必然是它的字尾的字首。(這句話稍微有點繞...)對於每一個sa[i]字尾,它的起始位置sa[i],那麼它最多能得到該字尾長度個子串(n-sa[i]個),而其中有height[i]個是與前一個字尾相同的,所以它能產生的實際字尾個數便是n-sa[i]-height[i]。遍歷一次所有的字尾,將它產生的字尾數加起來便是答案。
2、字尾的最長公共字首。(記為lcp(x,y))
這是height陣列的最基本性質之一。具體的可以參看羅穗騫的論文。字尾i和字尾j的最長公共字首的長度為它們在sa陣列中所在排位之間的height值中的最小值。這個描述可能有點亂,正規的說,令x=rank[i],y=rank[j],x<y,那麼lcp(i,j)=min(height[x+1],height[x+2]...height[y])。lcp(i,i)=n-sa[i]。解決這個問題,用RMQ的ST演算法即可(我只會這個,或者用最近公共祖先那個轉化的做法)。
3、最長重複子串(可重疊)
要看到,任何一個重複子串,都必然是某兩個字尾的最長公共字首。因為,兩個字尾的公共字首,它出現在這兩個字尾中,並且起始位置時不同的,所以這個公共字首必然重複出現兩次以上(可重疊)。而任何兩個字尾的最長公共字首為某一段height值中的最小值,所以最大為height值中的最大值(即某個lcp(sa[i],sa[i+1]))。所以只要算出height陣列,然後輸出最大值就可以了。
4、最長重複不重疊子串 PKU1743
這個問題和3的唯一區別在於能否重疊。加上不能重疊這個限制後,直接求解比較困難,所以我們選擇二分列舉答案,將問題轉換為判定性問題。假設當時列舉的長度為k,那麼要怎樣判斷是否存在長度為k的重複不重疊子串呢?
首先,根據height陣列,將字尾分成若干組,使得每組字尾中,字尾之間的height值不小於k。這樣分組之後,不難看出,如果某組字尾數量大於1,那麼它們之中存在一個公共字首,其長度為它們之間的height值的最小值。而我們分組之後,每組字尾之間height值的最小值大於等於k。所以,字尾數大於1的分組中,有可能存在滿足題目限制條件的長度不小於k的子串。只要判斷滿足題目限制條件成立,那麼說明存在長度至少為k的合法子串。
對於本題,限制條件是不重疊,判斷的方法是,一組字尾中,起始位置最大的字尾的起始位置減去起始位置最小的字尾的起始位置>=k。滿足這個條件的話,那麼這兩個字尾的公共字首不但出現兩次,而且出現兩次的起始位置間隔大於等於k,所以不會重疊。
深刻理解這種height分組方法以及判斷重疊與否的方法,在後面的問題中起到舉足輕重的作用。
5、最長的出現k次的重複(可重疊)子串。 PKU3261
使用字尾陣列解題時,遇到“最長”,除了特殊情況外(如問題3),一般需要二分答案,利用height值進行分組。本題的限制條件為出現k次。只需判斷,有沒有哪一組字尾數量不少於k就可以了。相信有了我前面問題的分析作為基礎,這個應該不難理解。注意理解“不少於k次”而不是“等於k次”的原因。如果理解不了,可以找個具體的例子來分析分析。
6、最長迴文子串 ural1297
這個問題沒有很直接的方法可以解決,但可以採用列舉的方法。具體的就是列舉迴文子串的中心所在位置i。注意要分迴文子串的長度為奇數還是偶數兩種情況分析。然後,我們要做的,是要求出以i為中心的迴文子串最長為多長。利用字尾陣列,可以設計出這樣一種求法:求i往後的字尾與i往前的字首的最長公共字首。我這裡的表述有些問題,不過不影響理解。
要快速地求這個最長字首,可以將原串反寫之後接在原串後面。在使用字尾陣列的題目中,連線兩個(n個)字串時,中間要用不可能會出現在原串中,不一樣的非0號的字元將它們隔開。這樣可以做到不影響字尾陣列的性質。然後,問題就可以轉化為求兩個字尾的最長公共字首了。具體的細節,留給大家自己思考...(懶...原諒我吧,都打這麼多字了..一個多小時了啊TOT )
7、求一個串最多由哪個串複製若干次得到 PKU2406
具體的問題描述請參考PKU2406.這個問題可以用KMP解決,而且效率比字尾陣列好。
利用字尾陣列直接解決本題也很困難(主要是,就算二分答案,也難以解決轉變而成的判定性問題。上題也是),但可以同過列舉模板串的長度k(模板串指被複制的那個串)將問題變成一個字尾陣列可以解決的判定性問題。首先判斷k能否被n整除,然後只要看lcp(1,k+1)<span times="" new="" roman";="" mso-hansi-font-family:"times="" roman";mso-bidi-font-family:"times="" mso-font-kerning:0pt;"="" style="font-size: 12pt; font-family: 宋體;">(實際在用c寫程式時是lcp(0,k))是否為n-k就可以了。
為什麼這樣就行了呢?這要充分考慮到字尾的性質。當lcp(1,k+1)=n-k時,字尾k+1是字尾1(即整個字串)的一個字首。(因為字尾k+1的長度為n-k)那麼,字尾1的前k個字元必然和字尾k+1的前k個字元對應相同。而後綴1的第k+1..2k個字元,又相當於字尾k+1的前k個字元,所以與字尾1的前k個字元對應相同,且和字尾k的k+1..2k又對應相同。依次類推,只要lcp(1,k+1)=n-k,那麼s[1..k]就可以通過自複製n/k次得到整個字串。找出k的最小值,就可以得到n/k的最大值了。
8、求兩個字串的最長公共子串。Pku2774、Ural1517
首先區分好“最長公共子串”和“最長公共子序列”。前者的子串是連續的,後者是可以不連續的。
對於兩個字串的問題,一般情況下均將它們連起來,構造height陣列。然後,最長公共子串問題等價於後綴的最長公共字首問題。只不過,並非所有的lcp值都能作為問題的答案。只有當兩個字尾分屬兩個字串時,它們的lcp值才能作為答案。與問題3一樣,本題的答案必然是某個height值,因為lcp值是某段height值中的最小值。當區間長度為1時,lcp值等於某個height值。所以,本題只要掃描一遍字尾,找出字尾分屬兩個字串的height值中的最大值就可以了。判斷方法這裡就不說明了,留給大家自己思考...
題目及題解:
9、重複次數最多的重複子串 SPOJ 687,Pku3693
難度比較大的一個問題,主要是羅穗騫的論文裡的題解寫得有點含糊不清。題目的具體含義可以去參考Pku3693.
又是一題難以通過二分列舉答案解決的問題(因為要求的是重複次數),所以選擇樸素列舉的方法。先列舉重複子串的長度k,再利用字尾陣列來求長度為k的子串最多重複出現多少次。注意到一點,假如一個字串它重複出現2次(這裡不討論一次的情況,因為那是必然的),那麼它必然包含s[0],s[k],s[2*k]...之中的相鄰的兩個。所以,我們可以列舉一個數i,然後判斷從i*k這個位置起的長度為k的字串能重複出現多少次。判斷方法和8中的相似,lcp(i*k,(i+1)*k)/k+1。但是,僅僅這樣會忽略點一些特殊情況,即重複子串的起點不在[i*k]位置上時的情況。這種情況應該怎麼求解呢?看下面這個例子:
aabababc
當k=2,i=1時,列舉到2的位置,此時的重複子串為ba(注意第一位是0),lcp(2,4)=3,所以ba重複出現了2次。但實際上,起始位置為1的字串ab出現次數更多,為3次。我們注意到,這種情況下,lcp(2,4)=3,3不是2的整數倍。說明當前重複子串在最後沒有多重複出現一次,而重複出現了部分(這裡是多重複出現了一個b)。如果我這樣說你沒有看懂,那麼更具體地:
sa[2]=bababc
sa[4]=babc
lcp=bab
現在注意到了吧,ba重複出現了兩次之後,出現了一個b,而a沒有出現。那麼,不難想到,可以將列舉的位置往前挪一位,這樣這個最後的b就能和前面的一個a構成一個重複子串了,而假如前挪的一位正好是a,那麼答案可以多1。所以,我們需要求出a=lcp(i*k,(i+1)*k)%n,然後向前挪k-a位,再用同樣的方法求其重複出現的長度。這裡,令b=k-a,只需要lcp(b,b+k)>=k就可以了。實際上,lcp(b,b+k)>=k時,lcp(b,b+k)必然大於等於之前求得的lcp值,而此時答案的長度只加1。沒有理解的朋友細細體會下上圖吧。
10.多個串的公共子串問題 PKU3294
首先將串連線起來,然後構造height陣列,然後怎麼辦呢?
對,二分答案再判斷是否可行就行了。可行條件很直觀:有一組字尾,有超過題目要求的個數個不同的字串中的字尾存在。即,假如題目要求要出現在至少k個串中,那麼就得有一組字尾,在不同字串中的字尾數大於等於k。
11、出現或反轉後出現所有字串中的最長子串 PKU1226
12、不重疊地至少兩次出現在所有字串中的最長子串
之所以把兩題一起說,因為它們大同小異,方法在前面的題目均出現過。對於多個串,連起來;反轉後出現,將每個字串反寫後和原串都連起來,將反寫後的串和原串看成同一個串;求最長,二分答案後height分組;出現在所有字串中(反寫後的也行),判斷方法和10一樣,k=n而已;不重疊見問題4,只不過這裡對於每個字串都要進行檢驗而已。
13、兩個字串的重複子串個數。 Pku3415
我個人覺得頗有難度的一個問題。具體的題目描述參看Pku3415。
推薦大家根據我這個題解中的提示,自己思考得出本題的解法。作為本筆記的最後一個問題,我不打算說太多了。實在想不出來的,直接聯絡我吧,我的QQ:403231899,讓我知道你也是學習程式設計的,我就會加你了。
三、 字尾陣列的總結
用字尾陣列解題有著一定的規律可循,這是字尾的性質所決定的,具體歸納如下:
1、N個字串的問題(N>1)
方法:將它們連線起來,中間用不會出現在原串中的,互不相同的,非0號字元分隔開。
2、無限制條件下的最長公共子串(重複子串算是字尾們的最長公共字首)
方法:height的最大值。這裡的無限制條件是對子串無限制條件。最多隻能是兩個串的最長公共子串,才可以直接是height的最大值。
3、特殊條件下的最長子串
方法:二分答案,再根據height陣列進行分組,根據條件完成判定性問題。三個或以上的字串的公共子串問題也需要二分答案。設此時要驗證的串長度為len,特殊條件有:
3.1、出現在k個串中
條件:屬於不同字串的字尾個數不小於k。(在一組字尾中,下面省略)
3.2、不重疊
條件:出現在同一字串中的字尾中,出現位置的最大值減最小值大於等於len。
3.3、可重疊出現k次
條件:出現在同一字串中的字尾個數大於等於k。若對於每個字串都需要滿足,需要逐個字串進行判斷。
4、特殊計數
方法:根據字尾的性質,和題目的要求,通過自己的思考,看看用字尾陣列能否實現。一般和“子串”有關的題目,用字尾陣列應該是可以解決的。
5、重複問題
知道一點:lcp(i,i+k)可以判斷,以i為起點,長度為k的一個字串,它向後自複製的長度為多少,再根據具體題目具體分析,得出演算法即可。