zoukankan      html  css  js  c++  java
  • 后缀数组资料

    http://www.nocow.cn/index.php/%E5%90%8E%E7%BC%80%E6%95%B0%E7%BB%84

    后缀数组是字符串处理的一个重要工具。它由原字符串的所有后缀的字典排序而得,具有较高的检索效率。

    基本概念

    一、字符串的大小比较: 关于字符串的大小比较,是指通常所说的 “ 字典顺序 ” 比较, 也就是对于两个字符串 u 、v ,令 i 从 1 开始顺次比较 u[i] 和 v[i] ,如果u[i]=v[i] 则令 i 加 1 ,否则若 u[i]<v[i] 则认为 u<v ,u[i]>v[i] 则认为 u>v,比较结束。如果 i>len(u) 或者 i>len(v) 仍比较不出结果,那么若 len(u)<len(v)则认为 u<v , 若 len(u)=len(v) 则 认 为 u=v ,若 len(u)>len(v) 则 u>v 。

     注:从字符串的大小比较的定义看,字符串s的所有后缀中任其中一对(u,v)不可能会相等,因为必要条件 len(u) ≠ len(v)不可能满足。所以任一字符串s中有len(s)个互不相同的后缀。我们可以将s的所有后缀排列,利用 后缀数组sa 与 名次数组rank 储存。 

    二、后缀数组sa:将s的n个后缀从小到大排序后将 排序后的后缀的开头位置 顺次放入sa中,则sa[i]储存的是排第i大的后缀的开头位置。简单的记忆就是“排第几的是谁”。

    三、名次数组rank:rank[i]保存的是suffix(i){后缀}在所有后缀中从小到大排列的名次。则 若 sa[i]=j,则 rank[j]=i。简单的记忆就是“你排第几”。

    对于 后缀数组sa 与 名次数组rank ,有

    rank[ sa[i] ]=i (这是很重要的一点,通过sa与rank的关系可以求出后缀数组)
    
     

    由此可看出,后缀数组sa 与名次数组rank的关系为互逆关系。

    字符串aabaaaab的sa数组与rank数组

    [编辑]倍增算法

    一、主要思路:倍增,s[i..i + 2k − 1]的排名通过s[i..i + 2k − 1 − 1]和s[i + 2k − 1..i + 2k − 1]的排名得到。

    二、简要过程:已知每个长度为2k − 1的字符串的排名,则可作为每个长度为2k的字符串求排名的关键字xy,s[i..i + 2k − 1]第一关键字x为s[i..i + 2k − 1 − 1]的排名,第二关键字y为s[i + 2k − 1..i + 2k − 1]的排名。以字符串aabaaaab为例:

    1. k=0,对每个字符开始的长度为20 = 1的子串进行排序,得到rank[1..8]={1,1,2,1,1,1,1,2}
    2. k=1,对每个字符开始的长度为21 = 2的子串进行排序:由k=0的rank得关键字xy[1..8]={11,12,21,11,11,11,12,20},得到rank[1..8]={1,2,4,1,1,1,2,3}
    3. k=2,对每个字符开始的长度为22 = 4的子串进行排序:由k=1的rank得关键字xy[1..8]={14,21,41,11,12,13,20,30},得到rank[1..8]={4,6,8,1,2,3,5,7}
    4. k=3,对每个字符开始的长度为23 = 8的子串进行排序:由k=2的rank得关键字xy[1..8]={42,63,85,17,20,30,50,70},得到rank[1..8]={4,6,8,1,2,3,5,7}

    注意:在排序过程中,rank[]可以有相同排名,但是sa[]排第几是没有相同的(就像Excel的排序,sa相当于编号,rank相当于排名)。这点可以从程序中体现。建议读者跟踪一下程序体会一下。

    整个过程如图:

    倍增算法的计算过程

    三、时间复杂度分析:每一趟的计数排序的时间复杂度是O(n),排序的次数共log n次,总的时间复杂度为O(n log n)。

    算法代码如下:
    void sorting(int j)//基数排序
    {
    	memset(sum,0,sizeof(sum));
    	for (int i=1; i<=s.size(); i++) sum[ rank[i+j] ]++;
    	for (int i=1; i<=maxlen; i++) sum[i]+=sum[i-1];
    	for (int i=s.size(); i>0; i--) tsa[ sum[ rank[i+j] ]-- ]=i;//对第二关键字计数排序,tsa代替sa为排名为i的后缀是tsa[i] 
    
    	memset(sum,0,sizeof(sum));
    	for (int i=1; i<=s.size(); i++) sum[ rank[i] ]++;
    	for (int i=1; i<=maxlen; i++) sum[i]+=sum[i-1];
    	for (int i=s.size(); i>0; i--) sa[ sum[ rank[ tsa[i] ] ]-- ]= tsa[i]; //对第一关键字计数排序
    	//构造互逆关系 
    }
    
    void get_sa()
    {
    	int p;
    	for (int i=0; i<s.size(); i++) trank[i+1]=s[i];
    	for (int i=1; i<=s.size(); i++) sum[ trank[i] ]++;
    	for (int i=1; i<=maxlen; i++) sum[i]+=sum[i-1];
    	for (int i=s.size(); i>0; i--) 
    		sa[ sum[ trank[i] ]-- ]=i;
    	rank[ sa[1] ]=1;
    	for (int i=2,p=1; i<=s.size(); i++)
    	{
    		if (trank[ sa[i] ]!=trank[ sa[i-1] ]) p++;
    		rank[ sa[i] ]=p;
    	}//第一次的sa与rank构造完成
    	for (int j=1; j<=s.size(); j*=2)
    	{
    		sorting(j);
    		trank[ sa[1] ]=1; p=1; //用trank代替rank 
    		for (int i=2; i<=s.size(); i++)
    		{
    			if ((rank[ sa[i] ]!=rank[ sa[i-1] ]) || (rank[ sa[i]+j ]!=rank[ sa[i-1]+j ])) p++;
    			trank[ sa[i] ]=p;//空间要开大一点,至少2倍
    		}
    		for (int i=1; i<=s.size(); i++) rank[i]=trank[i];
    	}
    }

    最长公共前缀

    求出了rank和sa数组还不够,通常我们需要由rank与sa数组计算出一个辅助工具height数组——最长公共前缀(LCP)。

    height 数组: 定义height[i]=suffix(sa[i-1]) 和 suffix(sa[i]) 的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。

    由height数组可得,对于j和k ,不妨设rank[j]<rank[k], 则有以下性质:suffix(j) 和 suffix(k) 的最长公共前缀为 height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]] 中的最小值。
    

    以"aabaaaab"为例,求后缀"abaaaab"和后缀"aaab"的最长公共前缀,如图,可见其最长公共前缀等于1。

    计算后缀"abaaaab"和后缀"aaab"的最长公共前缀

    所以说,计算最长公共前缀是一个典型的RMQ问题。

    如果直接按照sa的顺序一个一个求解,每一次比较最坏的时间复杂度是O(len(s)),一共要比较len(s)次,所以时间复杂度是O(len(s)2)。这样求height数组是非常慢的,而且没有用到之前所说height数组的性质。

    那么,如何高效地计算后缀间的最长公共前缀呢?

    当然是使用之前所说的性质。定义h[i]为suffix(i)和前一名次后缀的最长公共前缀{sa[ rank[ i ]-1 ]}。由性质可得,

    h[i] ge h[i-1]-1

     简单的证明如下:设suffix(k)是排在suffix(i-1)前一位的后缀,则它们的最长公共前缀显然是h[i-1]。那么,suffix(k+1)显然将排在suffix(i)的前面。并且,suffix(k+1)&suffix(i) 相对于 suffix(k)&suffix(i-1)来说就是同时去掉了第一位,即少了一位的匹配数。所以suffix(i)和前一名次后缀的最长公共前缀至少是h[i-1]-1。

    显然,我们可以按照h数组的顺序计算height。时间复杂度分析:求一次height后位数-1,一共有len(s)个后缀,所以只能退len(s)次,也就是说,求解的时间复杂度是O(len(s))。

    算法代码如下:
    void get_height()
    {
    	for (int i=1,j=0; i<=s.size(); i++)//用j代替上面的h数组
    	{
    		if (rank[i]==1) continue;
    		for (; s[i+j-1]==s[ sa[ rank[i]-1 ]+j-1 ]; ) j++;//注意越界之类的问题 
    		height[ rank[i] ]=j;
    		if (j>0) j--;
    	}
    }
    

    [编辑]构造后缀树

    利用最长公共前缀数组(lcp数组),使用一个栈就可以构造出相应的后缀树。

    [编辑]后缀数组的应用

    先提出后缀数组的几种常用技巧:

    1. 建议多找找与height数组的关联。
    2. 将几个字符串贴在一起,用特殊符号间隔开:如aab与aaab,可合并成aab$aaab。
    3. 二分+分组(思想)的方法:枚举出答案后,就能将合法的情况划分到一个组判断。

    以下列举出几个后缀数组的应用供大家思考,也可在讨论页讨论。

    1. 求两个后缀的最长公共前缀
    2. 求字符串的可重叠的最长重复子串:如ababa可重叠的最长重复子串是aba
    3. 求字符串的不可重叠的最长重复子串:如ababa不可重叠的最长重复子串是ab [提示:想想建议3]
    4. 计算不相同子串的个数:如aaaa的不相同子串数是4
    5. 计算最长回文子串:如aabaaaab的最长回文子串是6(baaaab)。[提示:想想建议2]
    6. 求两个字符串的最长公共子串:如aaba与abac的最长公共子串是aba。
  • 相关阅读:
    js验证身份证号,超准确
    C#对象序列化与反序列化
    寒冰王座[HDU1248]
    A C[HDU1570]
    循环多少次?[HDU1799]
    Play on Words[HDU1116]
    Fengshui-[SZU_B40]
    Travel Problem[SZU_K28]
    Big Event in HDU[HDU1171]
    Count the Trees[HDU1131]
  • 原文地址:https://www.cnblogs.com/jiangjing/p/3240956.html
Copyright © 2011-2022 走看看