[Pku 2774] 字符串(六) {后缀数组的构造}
{
從這一篇開始介紹后綴數組
一個強大的字符串處理工具
可以先研讀羅穗騫的論文
后綴數組——處理字符串的有力工具
再行閱讀本文 本文僅作參考和補充
}
?
字符串的后綴很好理解
譬如對于字符串"aabaaaab"
后綴有{"b","ab","aab","aaab","aaaab","baaaab","abaaaab","aabaaaab"}
后綴數組就是存儲后綴的有序數組
字符串的大小是這樣規定的
字符串的比較是逐個相應字符進行比較(比較他們的ASCII碼)
直到有兩個字符不相等為止 ASCII碼大的字母所在字符串就大
為了處理長度不等的比較方便 我們給字符串末尾添加一個特殊的字符
要求它不同于任何一個字符串中的字符
這樣我們對于后綴的比較就總能在一個串結束前比較出不同
上面的字符串經過添加特殊字符后就是"aabaaaab$"
排序之后就是這樣
9 $
4 aaaab$
5 aaab$
6 aab$
1 aabaaaab$
7 ab$
2 abaaaab$
8 b$
3 baaaab$
注意到我們不用存儲每一個后綴
只要用一個數組存儲每個后綴開始的位置即可
最后我們得到這樣一個整型數組
9 4 5 6 1 7 2 8 3 我們把它記做Select數組
通過它我們可以得到排名為K的后綴的開始位置S0=Select[k]
為了運算方便 我們還需要得到它的逆運算Rank數組
其中Rank[i]就是從i開始的后綴的排名
為了敘述方便 下文中一般用后綴開始位置i替代從i這個位置開始的后綴
?
首先我們要解決的問題就是如何生成后綴數組
即生成Select數組或者Rank數組
直接暴力排序肯定是不行的 由于比較字符串的復雜度為O(N)
最終復雜度達到了O(N^2*Log2N)
在羅穗騫的論文中 介紹了兩種算法 倍增算法和DC3算法
其中 倍增算法比較通俗易懂 DC3算法效率更高
這里介紹倍增算法 通常 倍增算法已經可以解決大部分問題
倍增算法的核心思想是遞推 算法復雜度是O(NLog2N)
先排序長為2^K的分段 由兩個2^K的分段拼接 快速地排序長為2^(K+1)的分段
考慮4個長度為2^k的串A B C D
當我們比較得出A B C D的大小時 通過上面字符串大小比較的定義
就可以很容易地比較得出E=A+B與F=C+D的大小
譬如 A<C 直接得出 E<F;A>C得出 E>F;
A=C且B<D 可以得出E<F; A=C且B>D 可以得出E>F;
這就是由2^K推出2^(K+1)的原理
實現可以用雙關鍵字排序
我們排序的對象是Rank數組 因為Rank直接反映了字符串的大小關系
而且Rank數組是有范圍的 所以用線性時間排序中針對Rank設計的計數排序CountingSort
?基本理論介紹完了 開始逐行解釋代碼
1 i:=0;2 ?while not eoln do
3 begin
4 inc(i);
5 read(ch[i]);
6 a[i]:=ord(ch[i]);
7 end;
8 readln;
9 n:=i;
10 ?for i:=1 to n do
11 inc(w[a[i]]);
12 ?for i:=2 to 127 do
13 w[i]:=w[i]+w[i-1];
14 ?for i:=n downto 1 do
15 begin
16 s[w[a[i]]]:=i;
17 dec(w[a[i]]);
18 end;
19 j:=1;
20 r[s[1]]:=1;
21 ?for i:=2 to n do
22 begin
23 if a[s[i]]<>a[s[i-1]]
24 then inc(j);
25 r[s[i]]:=j;
26 end;
1-9行用字符數組讀入一個字符串
10-26行進行預處理 求出K=0即每個分段為單個元素時的Rank和Select數組
10-18行用了一趟計數排序 處理出Select[]
基本思想是對于序列中的每一元素x 確定數組中小于x的元素的個數
注意到14行循環是逆序的 這保證了計數排序的穩定性
19-26行運用逆運算關系得到Rank[]數組 處理了重復
下面開始倍增的過程
1 k:=j; j:=1;2 ?while k<n do
3 begin
4 move(r,tr,sizeof(r));
5 fillchar(w,sizeof(w),0);
6 for i:=1 to n do
7 inc(w[r[i+j]]);
8 for i:=1 to n do
9 w[i]:=w[i]+w[i-1];
10 for i:=n downto 1 do
11 begin
12 ts[w[r[i+j]]]:=i;
13 dec(w[r[i+j]]);
14 end;
15 fillchar(w,sizeof(w),0);
16 for i:=1 to n do
17 begin
18 r[i]:=tr[ts[i]];
19 inc(w[r[i]]);
20 end;
21 for i:=1 to n do
22 w[i]:=w[i]+w[i-1];
23 for i:=n downto 1 do
24 begin
25 s[w[r[i]]]:=ts[i];
26 dec(w[r[i]]);
27 end;
28 k:=1;
29 r[s[1]]:=1;
30 for i:=2 to n do
31 begin
32 if (tr[s[i]]<>tr[s[i-1]])
33 or(tr[s[i]+j]<>tr[s[i-1]+j])
34 then inc(k);
35 r[s[i]]:=k;
36 end;
37 j:=j*2;
38 end;
k紀錄的是當前Rank數組內有多少個不同元素
顯然 Rank數組內的元素全都不同時 排序就完成了
j代表當前分段的長度為2*j 每趟外循環要乘2
為了代碼比較好理解 沒有加入羅穗騫論文內的常數優化 犧牲了點速度
不過無傷大雅 不會造成TLE
排序時引入了2個臨時數組TempSelect[]和TempRank[]
TempSelect[]存儲第二關鍵字的排序結果
TempRank[]存儲上一次的Rank[]
由于計數排序是穩定的 我們只要分別對第二 第一關鍵字都排一次序即可
6-14行對第二關鍵字 也就是每個分段的后面一段字符串排序
16-27行對第一關鍵字 分段的前面一段排序
18行注意到第一次排序之后 原來元素的位置已經改變 所以要根據TempSelect[]重新賦值
同時在25行也要注意到這一點
28-36行生成新的Rank數組 判斷重復要判斷兩段是否分別相等
循環結束 后綴數組就生成好了
?
我們為了解決問題通常還需要另外一個工具Height數組
Height[]紀錄了兩個相鄰排名的后綴的最長公共前綴
最基本的一個性質是:任意兩個后綴suffix(j)和suffix(k)的最長公共前綴為
height[rank[j]+1] height[rank[j]+2] height[rank[j]+3]……height[rank[k]]中的最小值
這是一個典型的區間最小值問題(Range Minimum Query)
通過稀疏表可以在O(NLog2N)的時間內預處理 在O(1)的時間內回答
具體可以去參考相關文章
由于很多字符串的問題都是關于字符串的子串的問題
子串最簡單的描述方式是 后綴的前綴
我們通過得到任意兩個后綴的公共前綴 正是謀求一種通用解決方法
下面考慮Height數組如何生成
羅穗騫論文內給出了一個不等式
設H[i]=Height[Rank[i]] 則H[i]>=H[i-1]-1
這樣可以在O(N)的時間內求得H[] 通過求H[]可以得出Height[]
下面給出簡單的說明
首先設K1=Select[Rank[i-1]-1],K2=Select[Rank[i]-1] (代入H[],Height[]定義得出)
由于Suffix(i-1)和Suffix(i)僅僅相差頭部一個元素
則K1和I-1的最長公共前綴去掉頭部一個元素 顯然是I和K2的一個公共前綴
又因為最長公共前綴為最大的公共前綴 所以H[i]>=H[i-1]-1
最后給出Height數組的求解代碼 具體實現H[]可以虛設不求 直接給Height[Rank[]]賦值
1 j:=0;2 h[1]:=0;
3 ?for i:=1 to n do
4 if r[i]<>1
5 then begin
6 k:=s[r[i]-1];
7 while a[i+j]=a[k+j] do
8 inc(j);
9 h[r[i]]:=j;
10 if j>0 then dec(j);
11 end;
12 ?for i:=1 to n do
13 begin
14 write(h[i]:4,' ');
15 for j:=s[i] to n do
16 write(ch[j]);
17 writeln;
18 end;
順便給了輸出的程序代碼 可以用來檢驗程序是否寫對
?
最后給一個簡單的具體問題
Pku2774 求兩個串的最長公共子串(不是最長公共子序列)
一個簡單的做法是把兩個串拼接起來求最大的重復出現的子串
還要確保不是同一個字符串的兩個子串
具體的做法是用兩個字符連接字符串S1 S2得到S1$S2#
掃描Height數組 確保Select[i-1]和Select[i]不是同一個字符串的后綴時 得出最大的Height[i]
代碼如下
Long const maxn=180001;c=255;
var i,m,n,p,j,k,max,y:longint;
w,sa,tsa,r,tr,x,h:array[1..maxn]of longint;
ch:array[1..maxn]of char;
begin
i:=0;
while not eoln do begin inc(i); read(ch[i]); end;
inc(i); ch[i]:='$'; readln; y:=i;
while not eoln do begin inc(i); read(ch[i]); end;
n:=i; fillchar(w,sizeof(w),0);
for i:=1 to n do begin x[i]:=ord(ch[i]); inc(w[x[i]]); end;
for i:=2 to c do w[i]:=w[i]+w[i-1];
for i:=n downto 1 do begin sa[w[x[i]]]:=i; dec(w[x[i]]); end;
r[sa[1]]:=1; p:=1;
for i:=2 to n do
begin
if x[sa[i]]<>x[sa[i-1]] then inc(p);
r[sa[i]]:=p;
end;
m:=p; j:=1;
while m<n do
begin
fillchar(w,sizeof(w),0);
move(r,tr,sizeof(tr)); p:=0;
for i:=n-j+1 to n do begin inc(p); tsa[p]:=i; end;
for i:=1 to n do if sa[i]>j then begin inc(p); tsa[p]:=sa[i]-j; end;
for i:=1 to n do begin r[i]:=tr[tsa[i]]; inc(w[r[i]]); end;
for i:=2 to maxn do w[i]:=w[i]+w[i-1];
for i:=n downto 1 do begin sa[w[r[i]]]:=tsa[i]; dec(w[r[i]]); end;
r[sa[1]]:=1; p:=1;
for i:=2 to n do
begin
if (tr[sa[i]]<>tr[sa[i-1]])or(tr[sa[i]+j]<>tr[sa[i-1]+j])
then inc(p); r[sa[i]]:=p;
end;
m:=p; j:=j*2;
end;
h[1]:=0; j:=0;
for i:=1 to n do
if r[i]<>1 then begin
k:=sa[r[i]-1];
while ch[k+j]=ch[i+j] do inc(j);
h[r[i]]:=j;
if j>0 then dec(j);
end;
{for i:=1 to n do
begin
write(h[i],' ');
for j:=sa[i] to n do
write(ch[j]);
writeln;
end; }
max:=0;
for i:=1 to n-1 do
if (sa[i]<y)and(sa[i+1]>y)or(sa[i]>y)and(sa[i+1]<y)
then if h[i+1]>max then max:=h[i+1];
writeln(max);
readln;
end.
(老早的代碼了 比較難看...)
下一篇具體介紹后綴數組的使用技巧
?
BOB HAN原創 轉載請注明出處 http://www.cnblogs.com/Booble/
轉載于:https://www.cnblogs.com/Booble/archive/2010/12/10/1902730.html
總結
以上是生活随笔為你收集整理的[Pku 2774] 字符串(六) {后缀数组的构造}的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nls_lang.sh: 114: [[
- 下一篇: varnish-cache使用