{
从这一篇开始介绍后缀数组
一个强大的字符串处理工具
可以先研读罗穗骞的论文
再行阅读本文 本文仅作参考和补充
}
字符串的后缀很好理解
譬如对于字符串"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]
代码如下
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/