在這之前我對串的全部認識就是kmp一家,應該說這次學習尾碼數組總算是讓我對串這種東西有了個基本的認識吧。
之前有聽說過尾碼樹和尾碼數組,感覺上跟線段樹樹狀數組的關係差不多,不過貌似尾碼數組的應用範圍也很廣吧
好了,進入正題
尾碼數組,顧名思義,就是由一些尾碼組成的數組,這些尾碼就是原串中以i開頭的尾碼
但單是這麼個數組沒用,我們要使他有一些性質才有用,怎麼做呢?排序?
沒錯,就是排序,可以感性地想想,如果對這些尾碼按字典序排序,是不是排完序後相鄰的兩個尾碼會是最“相似”的?
具體的性質稍後討論,先看看怎麼對其排序
如果裸的排序比較的話,每次比較的複雜度是O(n)的,那麼總的複雜度達到了O(n^2logn),相當差,需要考慮更強大的方法
這裡介紹一下針對這個問題的倍增演算法,它的複雜度可以做到O(nlogn),基本上可以搞定大部分題目了
這個演算法大概就是利用字典序的特殊比較方法和倍增的思想,減少了比較的次數,並充分利用上了之前的比較結果,所以最佳化了原演算法
先講兩個個很顯然的性質吧:
1.若兩個尾碼的前幾位比較已經出結果了,那麼就不用比下去了
2.對於兩個長度為2k的串,若知道每對長為k的串的大小關係,就可以O(1)的判斷出這兩個串的大小關係
利用簡單的性質構造出神奇的演算法,或許這就是oi的誘人之處吧
具體演算法如下(蒯自集訓隊論文2009羅穗騫)
用倍增的方法對每個字元開始的長度為2k 的子字
符串進行排序,求出排名,即rank 值。k 從0 開始,每次加1,當2k 大於n 以
後,每個字元開始的長度為2^k 的子字串便相當於所有的尾碼。並且這些子字
符串都一定已經比較出大小,即rank 值中沒有相同的值,那麼此時的rank 值就
是最後的結果。每一次排序都利用上次長度為2^(k-1) 的字串的rank 值,那麼長
度為2k 的字串就可以用兩個長度為2^(k-1) 的字串的排名作為關鍵字表示,然
後進行基數排序,便得出了長度為2^k 的字串的rank 值
如果排序用基數排序的話,總的複雜度就是O(nlogn)了
尾碼數組還有一個叫DC3的演算法,複雜度是O(n),但那個演算法中過多排序讓常數不太漂亮,而且編程複雜度大些,所以不做介紹了,有興趣可以去看看上面提到的那篇論文
尾碼數組中還有一個很重要的東西:height[]它表示尾碼數組中rank[i]和rank[i-1]的lcs的長度
有了下面這個結論height數組就很容易求了
height[rank[i]]>=height[rank[i]-1]-1
嚴格證明網上有,我是這樣理解的,尾碼數組中相鄰的兩個是“最相似”的,第i個和第i-1個的lcs都是k了,那i+1和i的lcs至少也是k-1嘛,(好像有點萎的解釋,其實這裡我還沒理解透)不管怎麼樣,這樣求就可以做到O(n)了
下面是我的模板
program syj;<br />uses math;<br />const<br /> maxn=100000+5;<br />type<br /> arr=array[0..maxn]of longint;<br />var ans,n,o:longint;<br /> a,b,sa,rank,h,bb,count:arr;<br /> st:ansistring;<br />procedure sort(var a:arr);<br />var i:longint;<br />begin<br /> fillchar(count,sizeof(count),0);<br /> for i:=1 to n do inc(count[a[i]+1]);<br /> for i:=1 to o do inc(count[i],count[i-1]);<br /> for i:=1 to n do begin<br /> inc(count[a[sa[i]]]);bb[count[a[sa[i]]]]:=sa[i];<br /> end;<br /> sa:=bb;<br />end;<br />procedure getrank;<br />var i:longint;<br />begin<br /> o:=0;<br /> for i:=1 to n do begin<br /> if (a[sa[i]]<>a[sa[i-1]])or(b[sa[i]]<>b[sa[i-1]]) then inc(o);<br /> rank[sa[i]]:=o;<br /> end;<br />end;<br />procedure work;<br />var i,l,j,k,last:longint;<br />begin<br /> //suffixarray<br /> o:=26;<br /> for i:=1 to n do begin<br /> a[i]:=ord(st[i])-96;b[i]:=0;sa[i]:=i;<br /> end;<br /> sort(a);getrank;<br /> l:=1;<br /> while o<>n do begin<br /> for i:=1 to n do begin<br /> a[i]:=rank[i];<br /> if i+l<=n then b[i]:=rank[i+l]<br /> else b[i]:=0;<br /> end;<br /> sort(b);sort(a);getrank;<br /> l:=l*2;<br /> end;<br /> last:=0;<br /> for i:=1 to n do begin<br /> if last>0 then dec(last);<br /> if rank[i]=1 then continue;<br /> j:=i+last;k:=sa[rank[i]-1]+last;<br /> while (j<=n)and(k<=n)and(st[j]=st[k]) do begin<br /> inc(j);inc(k);inc(last);<br /> end;<br /> h[rank[i]]:=last;<br /> ans:=max(ans,last);<br /> end;<br /> writeln(ans);<br />end;<br />begin<br /> assign(input,'input.txt');reset(input);<br /> assign(output,'output.txt');rewrite(output);<br /> readln(st);<br /> n:=length(st);<br /> work;<br /> close(input);<br /> close(output);<br />end.<br />
我們使用尾碼數組的時候要特別想清楚該用SA[I]還是RANK[I]還是就是I,不然會錯的很冤枉的
還有有個注意的地方倍增的時候,只要每個元素的標號都不同就可以結束了,即while o<>n do 而不必寫成 while l<n do
這兩個寫法常數差距很大(實測5~10倍的區別)
下面簡單介紹幾個簡單應用:
0.求一個串的最長可重疊子串:直接掃一遍height數組即可
1.求兩個串的最長公用子串(PKU 2774):
把兩個串聯起來,中間搞個不可能出現的字元,最後掃一遍height數組取個max即可,只有一個要求,SA[RANK[I]]和SA[RANK[I]-1]必須屬於兩個不同的串
2.求一個串的最長不重疊字串(PKU1743的核心演算法):
(蒯自zfy0701部落格)
現在我們假設-最長,不重疊,重複-子串的長度為len,那麼在一個height數組中有一些hgt[i]會小於len,在這個i左右的兩個子串,他們LCP是不可能大於或等於len的,這樣,就可以吧height數組看做很多LCP >= len的段,我們在每一段中進行掃描,記錄這一段中最大和最小的子串串索引(sa[x])(注意,算i的時候要同時取sa[i-1]和sa[i]來更新),如果兩者之和小於len,說明重疊了,否則就找到一個可行解。
易證:如果該串存在len1的不重疊子串,且len1 > len2,則該串也存在長度len2的不重疊子串,解有連續性,所以我們在上面這個過程外我們可以二分枚舉解。
對了,二分尋找之前之前可先調用尋找len = 4是否成立,不成立就直接return了。
這道題的關鍵點就是:判斷解的時候對height數組進行分段處理,這個分段的思想與其說重要,還不說就是height數組的基本性質。
3.求兩個字串不小於k的公用子串的個數(PKU3415):
此題較難,可直接看http://hi.baidu.com/zfy0701/blog/item/f2278a0928991dca3bc763a0.html寫的很清楚
貌似字串還有一些神級演算法什麼什麼自動機來的,暫時就不搞了吧
下一個目標,計算幾何