[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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465185.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux下I2C驱动发送IO时序,笔记四:linux下IO口模拟实现I2C协议

一、i2c总线是什么&#xff1f;1、i2c总线是一种物理总线及实实在在的总线&#xff0c;通过板子pcb等图能看到。2、i2c总线是一种主从结构。3、i2c总线是一种通信协议。4、i2c总线是两线制半双工串行总线&#xff1a;两线制&#xff1a;数据线(SDA)——>数据传输、时钟线(SC…

rails3异步发邮件

actionmailer 3.0 结合ar_mailer_rails3 异步发送邮件3 次投票jerry 分享于 3 个月 前, 0 条回复, 244 次浏览Share|actionmailer 3.0 的用法有所改进&#xff0c;并结合ar_mailer_rails3做异步邮件的发送&#xff0c;是比较方便的一种选择&#xff0c;把邮件都交给后台任务来发…

我见过的极品代码bug

程序员写代码有bug是很难避免的&#xff0c;也是因为程序员的bug存在&#xff0c;才让另一个职业有了更好的发展&#xff0c;那就是软件测试行业。但是程序员写bug&#xff0c;对于自己肯定是一件非常难忘的事情。我记得我在之前做一个TP固件升级的功能&#xff0c;也就是开机的…

Linux监控服务并主动重启

Linux查询后台进程&#xff0c;如果没有进程号&#xff0c;则重启服务&#xff1a; #!/bin/sh basepath$(cd dirname $0; pwd) while true doprocnumps -ef|grep "服务名称"|grep -v grep|wc -lif [ $procnum -eq 0 ]then#启动命令echo date %Y-%m-%d date %H:%M:%S …

双一流大学毕业的我,应该何去何从?

内卷时代&#xff0c;万物皆可卷&#xff01;什么是内卷&#xff0c;比如&#xff0c;你在电影院看电影&#xff0c;有一个人站起来了&#xff0c;那么就会有第二个人站起来&#xff0c;没一会儿所有的人就站起来了&#xff0c;但是和坐着相比&#xff0c;站着看电影体验感很差…

接上电就工作的单片机

对于手边的一盒STC单片机&#xff0c;测试一下它们使用相同的ISP方案在下载过程中是否都可以按照相同的方式完成程序的下载。同时对它们建立各自的元器件库&#xff0c;以便于后期的应用。通过这个实验&#xff0c;看到STC的单片机使用起来真实简捷明了&#xff0c;接上电就工作…

我的高中好友

很少写高中同学相关的文章&#xff0c;愿意是现在的生活上和原来的同学交集很少&#xff0c;他们的工作和我做的工作也有很大的差距。再一个是距离远&#xff0c;节假日不能互相走动。昨晚&#xff0c;我开直播。那时候是凌晨1点&#xff0c;然后我看到我一个高中的好朋友进了我…

类的初始化顺序详解

前言 先声明一个常识&#xff0c;类域和局部变量初始化的差异如下&#xff0c; 局部变量不初始化会报错&#xff1a; 类中属性(也称域)不赋初值&#xff0c;默认为0&#xff0c;如果是引用默认为空。 正题 一、类中属性按照定义的顺序初始化 我们声明一个Child类&#xff0c…

工程师的电脑桌面

【0】【1】【2】【3】【4】【5】【6】【7】【8】【9】【10】【11】【12】【13】【14】声明&#xff1a;本文素材来源网络&#xff0c;版权归原作者所有。如涉及作品版权问题&#xff0c;请与我联系删除。------------ END ------------转自黄工的公众号推荐阅读&#xff1a;专辑…

Jmeter JAVA请求

Jmeter Java请求 一、为什么要使用Java请求 现有的post 、 get 无法对加解密及鉴权的接口进行测试&#xff0c;需要进行封装&#xff0c;然后再请求&#xff0c;怎么进行封装&#xff1f;通常使用的方式是使用httpclient 工具类型来操作&#xff0c;下面举一个简单的Java自定义…

我的微信群里有一个外国人

最近&#xff0c;不知道怎么滴&#xff0c;我的微信群里面多了一个外国华人&#xff0c;然后他在群里面分享了他在国外的生活。实话说&#xff0c;我不曾羡慕过在美国生活的人们&#xff0c;我害怕走在街上突然遇到的枪击事件&#xff0c;也害怕每天面对的肯德基汉堡&#xff0…

第一次收到这么用心的感谢信

今天收到一个读者的感谢信心里特别暖&#xff0c;发出来给大家看看说明这很长时间的付出&#xff0c;说的很多话&#xff0c;是对别人是有帮助的&#xff0c;我也希望能够帮助到更多的人&#xff0c;希望大家有方向&#xff0c;有自己对生活的态度&#xff0c;然后坚持自己的理…

android fragment 底部菜单栏,一句话搞定Android底部导航栏,一键绑定Fragment、ViewPager...

现在大多数App都会用到底部导航栏&#xff0c;比如常见的聊天工具QQ、微信、购物App等等&#xff0c;有了底部导航栏&#xff0c;用户可以随时切换界面&#xff0c;查看不同的内容。它的实现方式也很多&#xff0c;以前大多使用TabHost来实现&#xff0c;但是现在我们有很多更好…

Http协议之报文·方法·状态码

【要点】 1. HTTP协议的主要特点2. HTTP报文的组成部分3. HTTP方法4. POST 和 GET的区别5. HTTP状态码【总结】HTTP协议的主要特点 主要特点&#xff1a;简单快速&#xff0c; 灵活&#xff0c; 无连接&#xff08;非keep-alive&#xff09;&#xff0c;无状态 每个资源URI是固…

MyEclipse 深色主题

Eclipse 是不支持主题的&#xff0c;不过可以通过导入导出perference文件来实现修改文本编辑器配色。 这里是一个深色主题&#xff0c;抓个图给大家看看&#xff1a; 下载地址&#xff1a;http://blog.codefront.net/2006/09/28/vibrant-ink-textmate-theme-for-eclipse/ 这里还…

深入理解嵌入式中重要的编程模型

大家好&#xff0c;我是写代码的篮球球痴。今天我们看一看业界一些著名的编程模型。背景模型是对事物共性的抽象&#xff0c;编程模型就是对编程的共性的抽象。什么是编程的共性呢&#xff1f;最重要的共性就是&#xff1a;程序设计时&#xff0c;代码的抽象方式、组织方式或复…

android v4包自动导入吧,android如何导入v4包的源码

1.我们导入v4包源码却发现没有导入按钮当我们调用android-support-v4.jar里面的控件的时候(这里以android.support.v4.view.ViewPager举例说明)&#xff0c;很多时候还需要查看此控件的源码&#xff0c;我们按住Ctrl键点击如下图中的ViewPager之后会出现如下提示出现这个问题的…

【floyd】【bitset】洛谷 P1841 [JSOI2007]重要的城市 题解

bitset玄学完美优化复杂度&#xff1f; 题目描述 参加jsoi冬令营的同学最近发现&#xff0c;由于南航校内修路截断了原来通向计算中心的路&#xff0c;导致去的路程比原先增加了近一公里。而食堂门前施工虽然也截断了原来通向计算中心的路&#xff0c;却没有使路程增加&#xf…

新风口下:嵌入式AI学习中较好的练手项目(附代码资料/学习视频/学习规划)...

有粉丝问我&#xff1a;“当前乃至未来5-10年&#xff0c;嵌入式开发者还有哪些风口&#xff1f;”画外音&#xff1a;风口的本质&#xff0c;其实就是一段时间的人才供需不平衡。说白了就是由于行业突变&#xff0c;敏锐的资本快速进入&#xff0c;导致短时间内行业大量扩张&a…

Windows 任务栏缩略图自定义程序[更新 Build20100830]

很久没有写一点小玩意儿了&#xff0c;今天终于有了一次机会。这个程序能够对 Windows 7 中的任务栏实时预览缩略图进行一系列个性化的调整&#xff0c;使其使用起来更炫更方便&#xff0c;避免了不方便的注册表修改操作&#xff0c;将其转化为方便图形界面&#xff0c;只需要点…