一些定义:设字符串S的长度为n,S[0~n-1]。

子串:设0<=i<=j<=n-1,那么由S的第i到第j个字符组成的串为它的子串S[i,j]。

后缀:设0<=i<=n-1,那么子串S[i,n-1]称作它的后缀,用Suffix[i]表示。

串比较:对于两个串S1,S2,设长度分别为n1,n2。若存在一个位置i,使得对于0<=j<i满足S1[j]=S2[j]且S1[i]<S2[i],那么我们称S1<S2。如果S1是S2的一个前缀,那么也有S1<S2。两个串相等当且仅当长度相同且所有位置的字母都相同。所以,对于S的任意两个不同的后缀Suffix[i],Suffix[j] ,它们一定是不相等的,因为它们的长度都不同。

后缀数组:设我们用数组sa表示S的后缀数组,0<=sa[i]<=n-1,表示将S的n个后缀从小到大排序后,排名第i的后缀的位置是sa[i]。

名次数组:设我们用数组rank表示S的名次数组,0<=rank[i]<=n-1,表示将S的n个后缀从小到大排序后,后缀Suffix[i]的排名是rank[i]。很明显,sa[rank[i]]=i。

现在我们的问题是,给出一个字符串S,长度为n,S[0~n-1],字符集大小为m。求出S的后缀数组sa。有两种方法计算这个sa,倍增法和DC3法。我们设m<=n(一般情况也是这样子的吧。。)。那么倍增法的时间复杂度是O(nlog(n)),DC3的时间复杂度是O(n)。两个方法的空间复杂度都是O(n)。

倍增法

倍增法的思路是:

(1)首先计算S[0],S[1],...,S[n-1]的排名(注意这个单个字符的排序)。比如,对于aabaaaab,排序后为:1,1,2,1,1,1,1,2

(2)计算子串S[0,1],S[1,2],S[2,3],...,S[n-2,n-1],S[n-1,null] 的排名(注意最后一个的第二个字符为空),由于我们知道了单个字符的排名, 那么每个子串可以用一个二元组来表示,比如S[0,1]={1,1},S[1,2]={1,2},S[2,3]={2,1},等等,也就是aa,ab,ba,aa,aa,aa,ab,b$(我们用$表示空)的排名,排序后为:1,2,4,1,1,1,2,3

(3)计算子串S[0,1,2,3],S[1,2,3,4],S[2,3,4,5],...,S[n-4,n-3,n-2,n-1],S[n-3,n-2,n-1,$],S[n-2,n-1,$,$],S[n-1,$,$,$]。方法与上面相同。依次类推,每次使用两个2^(x-1)长度的子串来计算2^x次方长度的子串的排名,直到某一次排序后n个数字各不相同。最后,对于串aabaaaab,如下所示

在实现的时候,一般在串的最后加一个空字符。这个字符比其他任何字符都小。下面是一份实现的程序

 class SuffixArray
{
private:
static const int N=; /**字符串最大长度**/
int wa[N],wb[N],wd[N],r[N]; bool isSame(int *r,int a,int b,int len)
{
return r[a]==r[b]&&r[a+len]==r[b+len];
}
void da(int *r,int *sa,int n,int m)
{
int *x=wa,*y=wb,*t;
for(int i=;i<m;i++) wd[i]=;
for(int i=;i<n;i++) wd[x[i]=r[i]]++;
for(int i=;i<m;i++) wd[i]+=wd[i-];
for(int i=n-;i>=;i--) sa[--wd[x[i]]]=i;
/**基数排序计算长度为1的子串的排名
相同的 越靠前 排名越小**/
for(int j=,p=;p<n;j<<=,m=p)
{
p=;
for(int i=n-j;i<=n-;i++) y[p++]=i;
for(int i=;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;
/**y[i]表示对于组成2^j的所有子串的二元组
{pi,qi}来说,第二关键字即qi排名为i的位置为y[i] **/ for(int i=;i<m;i++) wd[i]=;
for(int i=;i<n;i++) wd[x[i]]++;
for(int i=;i<m;i++) wd[i]+=wd[i-];
for(int i=n-;i>=;i--) sa[--wd[x[y[i]]]]=y[i];
/**这里倒着枚举 当两个位置的y[i]和y[j]对应的x
相同时,后面的排名大,因为它的第二关键字
即y的排名大 而x在外面也决定了排名以第一
关键字为主 **/
t=x;x=y;y=t;p=;
x[sa[]]=;
for(int i=;i<n;i++) x[sa[i]]=isSame(y,sa[i-],sa[i],j)?p-:p++;
}
}
public:
/**字符串S,长度n,S[0,n-1],为方便,我们假设它只包含小写字母
最后的后缀数组存储在sa[1~n]中 0<=sa[i]<=n-1
**/
void calSuffixArray(char *S,int n,int *sa)
{
for(int i=;i<n;i++) r[i]=S[i]-'a'+;
r[n++]=;
da(r,sa,n,);
}
};

DC3算法

(1)将所有的后缀分成两部分,一部分是模3不等于0的,比如Suffix[1],Suffix[2],Suffix[4],Suffix[7]等,第二部分是模3等于0的后缀,Suffix[0],Suffix[3]等。首先计算第一部分每个后缀的排名(计算的时候假设没有第二部分的这些后缀)。方法是将Suffix[1]和Suffix[2]连起来( 连起来之前要把Suffix[1]和Suffix[2]的长度都变为3的倍数,如果不是,就在后面补上字符集中没有且小于字符集中所有字符的字符,比如0)。对于串S=aabaaaaba,Suffix[1]=abaaaaba,Suffix[2]=baaaaba,分别补成3的倍数,Suffix[1]=abaaaaba0,Suffix[2]=baaaaba00,最后拼成的串为abaaaaba0baaaaba00 。如下所示

然后从前向后,每三个字符一组,即aba,aaa,ba0,baa,aab,a00,我们发现,他们分别是Suffix[1],Suffix[4],Suffix[7],Suffix[2],Suffix[5],Suffix[8] 的前3个字母。我们求出这六个的排名为4,2,5,6,3,1(注意,如果排序后还有相同的数字,也就是还有两个相同的串,比如3,2,4,5,2,1,那么要继续求,因为两个2之后的数字4大于1,所以第二的位置的2代表的后缀大于第5个位置的2代表的后缀。其实这个问题跟刚才的问题是相同的,所以可以递归求)。这样,我们最后得到了所有模3不等于0的位置的后缀的排名。

(2)计算模3等于0的位置的排名。这些位置的后缀,可以看做一个字符加上某个第一部分的一个后缀,这也很容易通过一次基数排序(就像倍增法的二元组一样)求得。对于上面的串,模3为0的后缀的排名为Suffix[9]<Suffix[3]<Suffix[0]<Suffix[6]

(3)合并第一部分和第二部分的排名。注意,上面求出的第一部分第二部分的排名都没有考虑另外一部分。合并的时候我们需要比较第一部分的某个后缀和第二部分的某个后缀。分两种情况。第一种是比较Suffix[3*i] 和Suffix[3*j+1],我们把它们看做:

Suffix[3*i]=S[3*i]+Suffix[3*i+1]

Suffix[3*j+1]=S[3*j+1]+Suffix[3*j+2]

Suffix[3*i+1]和Suffix[3*j+2]可以直接比较,因为它们都属于第一部分,而S[3*i]和S[3*j+1]也可以直接比较;

第二种情况是Suffix[3*i] 和Suffix[3*j+2],把它们看做是

Suffix[3*i]=S[3*i]+S[3*i+1]+Suffix[3*i+2]

Suffix[3*j+2]=S[3*j+2]+S[3*j+3]+Suffix[3*(j+1)+1]

Suffix[3*i+2]和Suffix[3*(j+1)+1]可以直接比较,它们都属于第二部分。而  前面是两个单个字符,可以直接比较。这样,就可以合并所有的后缀得到答案。

 class SuffixArray
{
private:
static const int N=; /**字符串最大长度**/
int wa[N*],wb[N*],wv[N*],ws[N*];
int r[N],sa[N]; int c0(int *r,int a,int b)
{
return r[a]==r[b]&&r[a+]==r[b+]&&r[a+]==r[b+];
} int c12(int k,int *r,int a,int b)
{
if(k==) return r[a]<r[b]||r[a]==r[b]&&c12(,r,a+,b+);
else return r[a]<r[b]||r[a]==r[b]&&wv[a+]<wv[b+];
} void sort(int *r,int *a,int *b,int n,int m)
{
for(int i=;i<n;i++) wv[i]=r[a[i]];
for(int i=;i<m;i++) ws[i]=;
for(int i=;i<n;i++) ws[wv[i]]++;
for(int i=;i<m;i++) ws[i]+=ws[i-];
for(int i=n-;i>=;i--) b[--ws[wv[i]]]=a[i];
} void dc3(int *r,int *sa,int n,int m)
{
#define F(x) ((x)/3+((x)%3==1?0:tb))
#define G(x) ((x)<tb?(x)*3+1:((x)-tb)*3+2) int *rn=r+n,*san=sa+n,ta=,tb=(n+)/,tbc=,p;
r[n]=r[n+]=;
for(int i=;i<n;i++) if(i%!=) wa[tbc++]=i; sort(r+,wa,wb,tbc,m);
sort(r+,wb,wa,tbc,m);
sort(r,wa,wb,tbc,m); rn[F(wb[])]=; p=;
for(int i=;i<tbc;i++) rn[F(wb[i])]=c0(r,wb[i-],wb[i])?p-:p++; if(p<tbc) dc3(rn,san,tbc,p);
else for(int i=;i<tbc;i++) san[rn[i]]=i;
/**以上是第一部分计算完毕*/ for(int i=;i<tbc;i++) if(san[i]<tb) wb[ta++]=san[i]*;
if(n%==) wb[ta++]=n-;
sort(r,wb,wa,ta,m);
/**以上是第二部分计算完毕*/ /**合并*/
for(int i=;i<tbc;i++) wv[wb[i]=G(san[i])]=i;
int i=,j=;
for(p=;i<ta&&j<tbc;p++)
{
sa[p]=c12(wb[j]%,r,wa[i],wb[j])?wa[i++]:wb[j++];
}
while(i<ta) sa[p++]=wa[i++];
while(j<tbc) sa[p++]=wb[j++]; #undef F(x)
#undef G(x)
} public:
/**字符串S,长度n,S[0,n-1],为方便,我们假设它只包含小写字母
最后的后缀数组存储在SA[1~n]中 0<=SA[i]<=n-1
**/
void calSuffixArray(char *S,int n,int *SA)
{
for(int i=;i<n;i++) r[i]=S[i]-'a'+;
r[n+]=;
dc3(r,sa,n+,);
for(int i=;i<=n;i++) SA[i]=sa[i];
}
};

到此位置,这就是求后缀数组sa的两种方式。 下面,来介绍一个新的数组height。

在求出sa数组之后,我们定义一个新的数组height,height[i]表示Suffix[sa[i-1]]与Suffix[sa[i]]的最长公共前缀,也就是排名为i和排名为i-1的两个后缀的最长公共前缀。如果我们求出了height数组,那么对于任意两个位置i,j,我们不妨设rank[i]小于rank[j],它们的最长公共前缀就是height[rank[i]+1],height[rank[i]+2],...,height[rank[j]]的最小值。比如字符串为aabaaaab,我们求后缀Suffix[1]=abaaaab和Suffix[4]=aaab的最长公共前缀,如下图所示

那么如何计算height?我们定义h[i]=height[rank[i]],也就是Suffix[i]和它前一名的最长公共前缀,那么很明显有h[i]>=h[i-1]-1。因为h[i-1]是Suffix[i-1]和它前一名的最长公共前缀,设为Suffix[k],那么Suffix[i]和Suffix[k+1] 的最长公共前缀为h[i-1]-1,所以h[i]至少是h[i-1]-1。所以我们可以按照求h[1],h[2],h[3] 顺序计算所有的height。代码如下

 const int N=;
int Rank[N]; void calHeight(int *r,int *sa,int n,int *height)
{
int i,j,k=;
for(int i=;i<=n;i++) Rank[sa[i]]=i;
for(int i=;i<n;i++)
{
if(k) k--;
j=sa[Rank[i]-];
while(i+k<n&&j+k<n&&r[i+k]==r[j+k]) k++;
height[Rank[i]]=k;
}
}

后缀数组:倍增法和DC3的简单理解的更多相关文章

  1. 【bzoj5073】[Lydsy1710月赛]小A的咒语 后缀数组+倍增RMQ+贪心+dp

    题目描述 给出 $A$ 串和 $B$ 串,从 $A$ 串中选出至多 $x$ 个互不重合的段,使得它们按照原顺序拼接后能够得到 $B$ 串.求是否可行.多组数据. $T\le 10$ ,$|A|,|B| ...

  2. 【bzoj3879】SvT 后缀数组+倍增RMQ+单调栈

    题目描述 (我并不想告诉你题目名字是什么鬼) 有一个长度为n的仅包含小写字母的字符串S,下标范围为[1,n]. 现在有若干组询问,对于每一个询问,我们给出若干个后缀(以其在S中出现的起始位置来表示), ...

  3. 【bzoj4516】[Sdoi2016]生成魔咒 后缀数组+倍增RMQ+STL-set

    题目描述 魔咒串由许多魔咒字符组成,魔咒字符可以用数字表示.例如可以将魔咒字符 1.2 拼凑起来形成一个魔咒串 [1,2].一个魔咒串 S 的非空字串被称为魔咒串 S 的生成魔咒. 例如 S=[1,2 ...

  4. UOJ.35.[模板]后缀排序(后缀数组 倍增)

    题目链接 论找到一个好的教程的正确性.. 后缀数组 下标从1编号: //299ms 2560kb #include <cstdio> #include <cstring> #i ...

  5. 洛谷.3809.[模板]后缀排序(后缀数组 倍增) & 学习笔记

    题目链接 //输出ht见UOJ.35 #include<cstdio> #include<cstring> #include<algorithm> const in ...

  6. POJ.3145.Common Substrings(后缀数组 倍增 单调栈)

    题目链接 \(Description\) 求两个字符串长度不小于k的公共子串对数. \(Solution\) 求出ht[]后先减去k,这样对于两个后缀A',B',它们之间的贡献为min{ht(A)}( ...

  7. POJ.2774.Long Long Message/SPOJ.1811.LCS(后缀数组 倍增)

    题目链接 POJ2774 SPOJ1811 LCS - Longest Common Substring 比后缀自动机慢好多(废话→_→). \(Description\) 求两个字符串最长公共子串 ...

  8. POJ.1743.Musical Theme(后缀数组 倍增 二分 / 后缀自动机)

    题目链接 \(Description\) 给定一段数字序列(Ai∈[1,88]),求最长的两个子序列满足: 1.长度至少为5 2.一个子序列可以通过全部加或减同一个数来变成另一个子序列 3.两个子序列 ...

  9. 后缀数组Da模板+注释 以及 dc3模板

    后缀数组Da模板: 1 /* 2 后缀数组倍增法Da板子 3 */ 4 #include <cstdlib> 5 #include <cstring> 6 #include & ...

随机推荐

  1. ActiveMQ2

    package com.winner.topic; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.Con ...

  2. C++:C++的两种多态形式

    // // main.cpp // Test.cpp // // Created by mac on 15/8/11. // Copyright (c) 2015年. All rights reser ...

  3. java://Comparator、Comparable的用法(按照要求将map集合的键值对进行顺序输出)

    import java.util.*; public class Person implements Comparable<Person>//使Person的属性具有比较性 { priva ...

  4. C#类继承和接口继承时一些模棱两可的问题[转]

    原文地址:http://www.cnblogs.com/harleyhu/archive/2012/11/29/2794809.html 1.在father定义的方法若含有virtual关键字,chi ...

  5. JavaScript增强AJAX基础

    <title>js类型</title> <meta http-equiv="content-type" content="text/html ...

  6. sed 引入shell变量

    双单引号即可 1.eval sed ’s/$a/$b/’ filename2.sed "s/$a/$b/" filename3.sed ’s/’$a’/’$b’/’ filenam ...

  7. VS工程添加资源文件

    1. 添加资源文件: 2. 资源文件内写相应代码: <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006 ...

  8. ios10 xcode8 适配的那些事

    1.首先open Url 废弃了. http://www.tuicool.com/articles/jiMr2qA

  9. 200行代码搞定炸金花游戏(PHP版)

    <?php/* * 游戏名称:炸金花(又名三张牌.扎金花) * 开发时间:2009.1.14 * 编 程:多菜鸟 * 来 源:http://blog.csdn.net/kingerq/archi ...

  10. Android tween 动画 XML 梳理

    前言: Tween动画是展现出旋转.渐变.移动.缩放的这么一种转换过程,即补间动画.Tween动画有两种定义方式:XML形式,编码形式.这次主要来梳理XML的方式配置动画 (1)XML定义动画,按照动 ...