最近感觉自己越来越蒟蒻了……后缀数组不会,费用流不会……

看着别人切一道又一道的题,我真是很无奈啊……

然后,我花了好长时间,终于弄懂了后缀数组。

后缀数组是什么?

后缀SASASA数组

给你一个字符串,让你将每个后缀排序,就是一个后缀数组

比如,字符串为ababa,就会搞出一个这样的东西:

a
aba
ababa
ba
baba
SA={4,2,0,3,1};

其中,每个后缀用开始的位置来表示。

rankrankrank数组

相当于逆着的SASASA,rank[sa[i]]=irank[sa[i]]=irank[sa[i]]=i

#后缀数组怎么求?

方法一:O(N3)O(N^3)O(N3)暴力

打个选择排序,每次比较用O(N)O(N)O(N)的方法。

当然,这样的暴力出不了奇迹。

方法二:O(N2lg⁡N)O(N^2\lg N)O(N2lgN)快排

仅仅是将选择排序变成快排罢了。

##方法三:倍增

倍增1.0

这就是本文的重点了!!!

想想其它的倍增是怎么做的,再想想字符串怎么倍增。

首先,给每个字符赋一个排名,像这样:

'a'->1
'b'->2

现在rank={1,2,1,2,1}

然后像下面的这幅图一样搞一搞。



倍增算法的精髓就在这幅图中。

枚举一个iii,对于每个位置jjj,每次将jjj和j+2ij+2^ij+2i合并到一起(不够补0),然后排名(可以当做是离散化)。

这样就可以求出rankrankrank,然后求出SASASA。

具体没什么好讲的,只要看懂这幅图,就懂了倍增算法的思想。

这样就可以优化到O(Nlg⁡2N)O(N\lg^2N)O(Nlg2N)了。

倍增2.0

然而,这不是倍增的极限。

可以用基数排序进一步地优化!!!

什么是基数排序,基数排序怎么打,请百度一下(基数排序可以用一维的数组来打,具体看代码实践)。

在此,我有意提醒的是,你可以将数看做一个m进制,在倍增合并两个数时,可以将其看做m进制的两位数,然后对它进行基数排序。

这样就有O(Nlg⁡N)O(N\lg N)O(NlgN)的时间复杂度了。

具体见代码。

DC3算法

笔者暂时不会……


后缀数组怎么打?

后缀数组其实是比较好理解的,但是,为了追求完美,我们不应光靠自己的理解打模板。

因为自己打的有时会非常丑陋……

看了,理解的网上的标,综合我自己的风格,就打出了个这样的标:

int y[2000003],ws[2000003],wv[2000003];
void getSA(char s[],int rank[],int sa[],int n,int m)
{
memset(ws,0,sizeof(int)*m);
memset(y,255,sizeof y);
memset(rank,255,sizeof rank);
for (int i=0;i<n;++i)
++ws[rank[i]=s[i]];
for (int i=1;i<m;++i)
ws[i]+=ws[i-1];
for (int i=n-1;i>=0;--i)
sa[--ws[rank[i]]]=i;
for (int i=1;p<n;i<<=1,m=p)
{
p=0;
for (int j=n-i;j<n;++j)
y[p++]=j;
for (int j=0;j<n;++j)
if (sa[j]>=i)
y[p++]=sa[j]-i;
for (int j=0;j<n;++j)
wv[j]=rank[y[j]];
memset(ws,0,sizeof(int)*m);
for (int j=0;j<n;++j)
++ws[wv[j]];
for (int j=1;j<m;++j)
ws[j]+=ws[j-1];
for (int j=n-1;j>=0;--j)
sa[--ws[wv[j]]]=y[j];
swap(rank,y);
p=1;
rank[sa[0]]=0;
for (int j=1;j<n;++j)
rank[sa[j]]=(y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]?p-1:p++);
}

这就是网上通常的打法,当然,风格会有些不一样……

是不是看了后,一头雾水?

别急,慢慢解释。

首先说一下,在这个程序中,rankrankrank取值是在[0,n)[0,n)[0,n)范围内的,和上面那张图不一样!

	memset(ws,0,sizeof(int)*m);
memset(y,255,sizeof y);
memset(rank,255,sizeof rank);
for (int i=0;i<n;++i)
++ws[rank[i]=s[i]];
for (int i=1;i<m;++i)
ws[i]+=ws[i-1];
for (int i=n-1;i>=0;--i)
sa[--ws[rank[i]]]=i;

前面三行赋初值。

这是处理最开始的rankrankrank和sasasa,也就是还没有合并时。

wswsws数组是一个桶,用于辅助基数排序。

注意第三行rank[i]=s[i]。我们在实践的时候一开始不需要将真正的排名弄出来,我们只需知道它们的相对大小。而sis_isi​作为单个字符,是可以表示它们的相对大小的。

其它就没什么了,要理解好一维的基数排序

for (int i=1,p=1;p<n;i<<=1,m=p)

iii表示的是对于一个位置jjj,在这一轮中要用jjj和j+ij+ij+i合并。

ppp表示不同的字符串的个数,初值设为111是为了循环条件p&lt;np&lt;np<n,显然1≥n1\geq n1≥n时就没必要做了。

为什么循环条件是p&lt;np&lt;np<n呢?因为我们发现,最后的rankrankrank数组一定是一个范围在[0,n)[0,n)[0,n)的排列

所以ppp顶多为nnn,想想,当p=np=np=n时,那么其实已经排好序了,没必要再做下去,比如,可以看看上面那张图,可以发现最后一轮是没有必要的。

mmm表示的也是不同字符串的个数,只是因为在下面ppp要被用作计数器罢了。

		p=0;
for (int j=n-i;j<n;++j)
y[p++]=j;
for (int j=0;j<n;++j)
if (sa[j]>=i)
y[p++]=sa[j]-i;

当初我看得最久的是这一段……

这其实是一个小优化。

yyy是一个临时的rankrankrank数组。

在合并后,其实第二关键字可以通过上一次的sasasa数组求出

先看看二、三行。显然,[n−i,n)[n-i,n)[n−i,n)这段区间内,如果要和后面的合并,只能补000,应该说是补−1-1−1,因为rankrankrank数组在这个程序中的取值是[0,n)[0,n)[0,n)。

−1-1−1一定是最小的,所以先把它们排在前面。

然后看倒数三行,这个就比较难理解了。

对于位置jjj,在这一轮中会对j−ij-ij−i有影响,所以说,j−i≥0j-i \geq 0j−i≥0

因为sasasa是有序的,所以我们顺序枚举sajsa_jsaj​,将其中满足以上条件的加入yyy中。

		for (int j=0;j<n;++j)
wv[j]=rank[y[j]];
memset(ws,0,sizeof(int)*m);
for (int j=0;j<n;++j)
++ws[wv[j]];
for (int j=1;j<m;++j)
ws[j]+=ws[j-1];
for (int j=n-1;j>=0;--j)
sa[--ws[wv[j]]]=y[j];

这一段的作用就是以第一关键字来进行一次基数排序,和上面的那个一样的道理。

		swap(rank,y);
p=1;
rank[sa[0]]=0;
for (int j=1;j<n;++j)
rank[sa[j]]=(y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]?p-1:p++);

ppp起到计数器的作用。

重点是最后一行y[sa[j-1]]==y[sa[j]] && y[sa[j-1]+i]==y[sa[j]+i]

这是在比较saj−1sa_{j-1}saj−1​和sajsa_jsaj​是否相等。如果相等,那么rankrankrank值应该要一样(不过注意,到最后时rankrankrank值一定是不同的!)

网上的标这样比较,就不怕爆掉吗?对此,我很不理解,只是开了两倍的数组来解决这个问题。


关于LCP

一些概念

LCP(i,j)LCP(i,j)LCP(i,j)表示suffix(i)suffix(i)suffix(i)和suffix(j)suffix(j)suffix(j)的公共最长前缀。

height(i)=LCP(SA[i−1],SA[i])height(i)=LCP(SA[i-1],SA[i])height(i)=LCP(SA[i−1],SA[i])

很明显,若ranki&lt;rankjrank_i&lt;rank_jranki​<rankj​,则

LCP(i,j)=min⁡k∈(ranki,rankj]heightkLCP(i,j)=\min_{k\in \left(rank_i,rank_j \right]}{height_k}LCP(i,j)=k∈(ranki​,rankj​]min​heightk​

如何求heightheightheight?

首先,我们要知道一个性质:

设hi=heightrankih_i=height_{rank_i}hi​=heightranki​​,也就是suffix(i)suffix(i)suffix(i)与它前一名的最长公共前缀。

那么hi≥hi−1−1h_i\geq h_{i-1}-1hi​≥hi−1​−1

证明:

设suffix(k)suffix(k)suffix(k)表示suffix(i−1)suffix(i-1)suffix(i−1)前一名的后缀,hi−1h_{i-1}hi−1​即是它们的最长公共前缀。

当hi−1≤1h_{i-1} \leq 1hi−1​≤1时,显然等式成立。

当hi−1&gt;1h_{i-1} &gt;1hi−1​>1时,可以发现suffix(k+1)suffix(k+1)suffix(k+1)和suffix(i)suffix(i)suffix(i)的公共后缀至少为hi−1−1h_{i-1}-1hi−1​−1。可以画张图理解一下。

所以,综上,hi≥hi−1−1h_i\geq h_{i-1}-1hi​≥hi−1​−1

利用这个性质,我们可以在O(N)O(N)O(N)的时间内求出heightheightheight数组

代码

void getheight(char s[],int rank[],int sa[],int height[])
{
for (int i=0,k=0;i<n;++i)
if (rank[i])
{
if (k)
--k;
int j=sa[rank[i]-1];
while (s[i+k]==s[j+k])
++k;
height[rank[i]]=k;
}
}

其实不必真正地构出个hhh数组。


其它

建议数组从111开始,或者将rankrankrank及辅助数组yyy初值设为−1-1−1,因为在比较时,后面的要补000(或−1-1−1),我就因为这样调了很久……(被罗穗骞大佬的论文坑了)

后缀数组(SA)及height数组的更多相关文章

  1. 关于后缀数组的倍增算法和height数组

    自己看着大牛的论文学了一下后缀数组,看了好久好久,想了好久好久才懂了一点点皮毛TAT 然后就去刷传说中的后缀数组神题,poj3693是进化版的,需要那个相同情况下字典序最小,搞这个搞了超久的说. 先简 ...

  2. 后缀数组的一些性质----height数组

    height数组:定义 height[i] = suffix[i-1] 和 suffix[i] 的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀.那么对于 j 和 k 不妨设 Rank[j] & ...

  3. bzoj3796(后缀数组)(SA四连)

    bzoj3796Mushroom追妹纸 题目描述 Mushroom最近看上了一个漂亮妹纸.他选择一种非常经典的手段来表达自己的心意——写情书.考虑到自己的表达能力,Mushroom决定不手写情书.他从 ...

  4. Distinct Substrings(spoj694)(sam(后缀自动机)||sa(后缀数组))

    Given a string, we need to find the total number of its distinct substrings. Input \(T-\) number of ...

  5. 后缀数组(SA)总结

    后缀数组(SA)总结 这个东西鸽了好久了,今天补一下 概念 后缀数组\(SA\)是什么东西? 它是记录一个字符串每个后缀的字典序的数组 \(sa[i]\):表示排名为\(i\)的后缀是哪一个. \(r ...

  6. 【后缀数组之height数组】

    模板奉上 int rank[maxn],height[maxn]; void calheight(int *r,int *sa,int n) { ; ;i<=n;i++) rank[sa[i]] ...

  7. 后缀数组SA学习笔记

    什么是后缀数组 后缀数组\(sa[i]\)表示字符串中字典序排名为\(i\)的后缀位置 \(rk[i]\)表示字符串中第\(i\)个后缀的字典序排名 举个例子: ababa a b a b a rk: ...

  8. 后缀数组入门(二)——Height数组与LCP

    前言 看这篇博客前,先去了解一下后缀数组的基本操作吧:后缀数组入门(一)--后缀排序. 这篇博客的内容,主要建立于后缀排序的基础之上,进一步研究一个\(Height\)数组以及如何求\(LCP\). ...

  9. 【POJ2774】Long Long Message(后缀数组求Height数组)

    点此看题面 大致题意: 求两个字符串中最长公共子串的长度. 关于后缀数组 关于\(Height\)数组的概念以及如何用后缀数组求\(Height\)数组详见这篇博客:后缀数组入门(二)--Height ...

随机推荐

  1. error C2872: 'ULONG_PTR' : ambiguous symbol

    转自VC错误:http://www.vcerror.com/?p=74 问题描述: 错误:error C2872: 'ULONG_PTR' : ambiguous symbol 解决方法: 详细的解决 ...

  2. iOS开发之SceneKit框架--SCNNode.h

    1.SCNNode简介 SCNNode是场景图的结构元素,表示3D坐标空间中的位置和变换,您可以将模型,灯光,相机或其他可显示内容附加到该元素.也可以对其做动画. 2.相关API简介 初始化方法 // ...

  3. Spring的refresh()方法相关异常

    如果是经常使用Spring,特别有自己新建ApplicationContext对象的经历的人,肯定见过这么几条异常消息:1.LifecycleProcessor not initialized - c ...

  4. centos7 将home目录空间扩容到根目录

    [root@localhost ~]# umount /home/ [root@localhost ~]# lvremove /dev/mapper/centos-home Do you really ...

  5. 最最最详细的IDEA导入Eclipse项目

    很详细的IDEA导入Eclipse项目,配置tomcat并运行项目 1.把Eclipse项目复制一份,放到自己指定的位置 2.打开Idea,在进入工程前选择,inmport Project 注意事项: ...

  6. 二分图最佳匹配KM算法 /// 牛客暑期第五场E

    题目大意: 给定n,有n间宿舍 每间4人 接下来n行 是第一年学校规定的宿舍安排 接下来n行 是第二年学生的宿舍安排意愿 求满足学生意愿的最少交换次数 input 2 1 2 3 4 5 6 7 8 ...

  7. 分布式日志收集之Logstash 笔记(一)

    (一)logstash是什么? logstash是一种分布式日志收集框架,开发语言是JRuby,当然是为了与Java平台对接,不过与Ruby语法兼容良好,非常简洁强大,经常与ElasticSearch ...

  8. 确认(confirm 消息对话框)语法:confirm(str); 消息对话框通常用于允许用户做选择的动作,如:“你对吗?”等。弹出对话框(包括一个确定按钮和一个取消按钮)

    确认(confirm 消息对话框) confirm 消息对话框通常用于允许用户做选择的动作,如:"你对吗?"等.弹出对话框(包括一个确定按钮和一个取消按钮). 语法: confir ...

  9. Luogu P1131 [ZJOI2007]时态同步(dfs)

    P1131 [ZJOI2007]时态同步 题意 题目描述 小\(Q\)在电子工艺实习课上学习焊接电路板.一块电路板由若干个元件组成,我们不妨称之为节点,并将其用数字\(1,2,3,\dots\).进行 ...

  10. mysql之备份表和备份数据库

    备份表 1.首先创建一个与原来一样的表 create table score2 like score; ###like就是将score表的结构拷贝过来,但是它并不执行数据:也就是说执行完上面的语句之后 ...