后缀数组的应用

本文参考了 后缀数组--处理字符串的有力工具

子串:字符串S 的子串r[i..j],i≤j,表示r 串中从i 到j 这一段,
也就是顺次排列r[i],r[i+1],...,r[j]形成的字符串。
后缀:后缀是指从某个位置i 开始到整个串末尾结束的一个特殊子串。字符串r 的从第i 个字符开始的后缀表示为Suffix(i) , 也就是
Suffix(i)=r[i..len(r)]

后缀数组:后缀数组SA 是一个一维数组,它保存1..n 的某个排列SA[1],
SA[2],……,SA[n],并且保证Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n。
也就是将S 的n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺
次放入SA 中名次数组:名次数组Rank[i]保存的是Suffix(i)在所有后缀中从小到大排
列的“名次”。
简单的说,后缀数组是“排第几的是谁?”,名次数组是“你排第几?”

用倍增算法:

倍增算法的主要思路是:用倍增的方法对每个字符开始的长度为2k 的子字
符串进行排序,求出排名,即rank 值。k 从0 开始,每次加1,当2k 大于n 以
后,每个字符开始的长度为2k 的子字符串便相当于所有的后缀。并且这些子字
符串都一定已经比较出大小,即rank 值中没有相同的值,那么此时的rank 值就
是最后的结果。每一次排序都利用上次长度为2k-1 的字符串的rank 值,那么长
度为2k 的字符串就可以用两个长度为2k-1 的字符串的排名作为关键字表示,然
后进行基数排序,便得出了长度为2k 的字符串的rank 值。

代码如下:

/*倍增算法   */
#define maxn 255
int wa[maxn],wb[maxn],wv[maxn],wsn[maxn];
/*最后一个为0  这样不会溢出的  哈哈*/

int cmp(int *r,int a,int b,int l)
{
	return r[a]==r[b]&&r[a+l]==r[b+l];
}
//r表示 rank数组
void da(int *r,int *sa,int n,int m)
{
	//由rank数组生成sa;
	int i,j,p,*x=wa,*y=wb,*t;
	
	for(i=0;i<m;i++)
		wsn[i]=0;
	for(i=0;i<n;i++)
		wsn[x[i]=r[i]]++;
	for(i=1;i<m;i++)
		wsn[i]+=wsn[i-1];
	for(i=n-1;i>=0;i--)
		sa[--wsn[x[i]]]=i;

	for(j=1,p=1;p<n;j*=2,m=p)
	{
		/*对第二个关键字排序  原数组的下标保存在y*/
		for(p=0,i=n-j;i<n;i++)
			y[p++]=i;
		for(i=0;i<n;i++)
			if(sa[i]>=j)
				y[p++]=sa[i]-j;
		/*对第一个关键字排序*/
		for(i=0;i<n;i++)
			wv[i]=x[y[i]];
		for(i=0;i<m;i++)
			wsn[i]=0;
		for(i=0;i<n;i++)
			wsn[wv[i]]++;
		for(i=1;i<m;i++)
			wsn[i]+=wsn[i-1];
		for(i=n-1;i>=0;i--)
			sa[--wsn[wv[i]]]=y[i];
		/*此时y用来保存rank  用sa生成新的rank 保存在x中*/
		for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
			x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;



	}
	



}


void fun_2(char *str,int len)/*生成长度为1的  rank*/
{
	int i,p;
	
	
	int *sa=new int[len];
	for(i=0;i<maxn;i++)
	{
		wsn[i]=0;
	}
	
	for(i=0;i<len;i++)
	{
		sa[i]=0;
		wsn[str[i]]++;
	}
	for(i=1;i<maxn;i++)
	{
		wsn[i]+=wsn[i-1];
	}
	
	for(i=len-1;i>=0;i--)
		sa[--wsn[str[i]]]=i;

	/*
	for(i=0;i<len;i++)
	{
		cout<<i<<"  sa="<<sa[i]<<endl;
	}*/

	int *r=new int[len];
	for(i=1,r[sa[0]]=0,p=1;i<len;i++)
	{
		if(str[sa[i]]==str[sa[i-1]])
			r[sa[i]]=p-1;
		else
			r[sa[i]]=p++;
	
	}
	cout<<"  hi  "<<endl;
	for(i=0;i<len;i++)
		cout<<i<<"  "<<r[i]<<endl;
	da(r,sa,len,maxn);
	//cout<<" len="<<len<<endl;
	for(i=0;i<len;i++)
		cout<<" i="<<i<<"  "<<sa[i]<<endl;
	
}
本算法用到基数排序 先对第二个关键字y 排序,在对第一个关键字y[i]排序 ,x是名次数组

用c++ sort生成后缀数组 代码如下:

void fun(string str)//调用sort生成后缀数组
{
	int len=str.size(),i,j;
	string *src=new string[len];
	for(i=0;i<len;i++)
	{
		src[i]=str.substr(i,len);//生成后缀数组
		//cout<<i<<"  "<<src[i]<<endl;
	}
    sort(src,src+len,cmp);
	
	cout<<" fun "<<endl;
	for(i=0;i<len;i++)
	{
		cout<<len-src[i].length()<<endl;
		cout<<src[i]<<" "<<endl;
	}
	delete []src;
		
}
height 数组:定义height[i]=suffix(sa[i-1])和suffix(sa[i])的最长公
共前缀,也就是排名相邻的两个后缀的最长公共前缀。那么对于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]]中的最小值。

那么应该如何高效的求出height 值呢?
如果按height[2],height[3],……,height[n]的顺序计算,最坏情况下
时间复杂度为O(n2) 。这样做并没有利用字符串的性质。定义
h[i]=height[rank[i]],也就是suffix(i)和在它前一名的后缀的最长公共前
缀。

h 数组有以下性质:
h[i]≥h[i-1]-1
证明:
设suffix(k)是排在suffix(i-1)前一名的后缀,则它们的最长公共前缀是
h[i-1]。那么suffix(k+1)将排在suffix(i)的前面(这里要求h[i-1]>1,如果
h[i-1]≤1,原式显然成立)并且suffix(k+1)和suffix(i)的最长公共前缀是

可以依次求h[0]  h[1]  h[n-1]    h[i]=height[rank[i]]   

代码如下:

struct information
{
	int *height;
	int *sa;
};
/*求height数组  h(i)=height[rank[i]]  h[i]>=h[i-1]*/
struct information fun_height(string str)//调用sort生成后缀数组
{
	int len=str.size(),i,j;
	information res;
	string *src=new string[len];
	for(i=0;i<len;i++)
	{
		src[i]=str.substr(i,len);//生成后缀数组
		//cout<<i<<"  "<<src[i]<<endl;
	}
    sort(src,src+len);
	
	/*生成rank数组  sa数组*/
	int *rank=new int[len];
	int *sa=new int[len];

	for(i=0;i<len;i++)
	{
		
		rank[len-src[i].length()]=i;
		
		//cout<<src[i]<<" "<<endl;
	}
	for(i=0;i<len;i++)
	{
		sa[rank[i]]=i;
	}
	/*cout<<"  sa "<<endl;
	for(i=0;i<len;i++)
	{
		cout<<sa[i]<<endl;
	}*/
	

	/*生成heights数组   h[i]=height[rank[i]]  则 h[i]>=h[i-1]-1*/
	int * height=new int[len];
	int k=0;
	for(i=0;i<len;i++)
	{
		if(rank[i]==0)
		{
			height[0]=0;
			k=0;
			
		}else
		{
			if(k>0) k--;
		
			int j=sa[rank[i]-1];
			while(str[i+k]==str[j+k]) k++;
			height[rank[i]]=k;
		}
	
	}
	
	
	delete []src;
	delete []rank;
	res.height=height;
	res.sa=sa;
	return res;


		
}
以上出后缀数组和height数组 

下面看几个后缀数组的例子:

回文问题:

考虑到回文长度为奇数 偶数的情况,代码如下:

void get_longest_huiwen2(string str)
{
	int len=str.size(),i,j,k,max=-0xffff, start;
	for(i=0;i<len;i++)
	{
		/*回文长度为偶数的时候*/
		k=i+1;j=i;
		int length=0;
		while(k<len&&j>=0)
		{
			if(str[k]==str[j])
			{
				k++;j--;
			}else
				break;
			length+=2;
			if(max<length)
			{
				max=length;
				start=j+1;
			}
		}
		/*长度为奇数的时候*/
		k=i+1,j=i-1,length=1;
		while(k<len&&j>=0)
		{
			if(str[k]==str[j])
			{
				k++;j--;
			}else
				break;
			length+=2;
			if(max<length)
			{
				max=length;
				start=j+1;
			}
		}
	}
	cout<<"get_longest_huiwen2  max  ="<<max <<endl;
	cout<<str.substr(start,max)<<endl;
}
用后缀数组的话,回文子串:如果将字符串L 的某个子字符串R 反过来写后和原来的字符串R一样,则称字符串R 是字符串L 的回文子串

代码如下:

/*最长回文子串*/
bool isok(int *sa,int i,int len)
{
	return (sa[i]<=(len-1)/2-1&&sa[i-1]>=(len+1)/2)||(sa[i-1]<=(len-1)/2-1&&sa[i]>=(len+1)/2);
}
void get_longest_huiwen(string str2)
{
	string s(str2.rbegin(),str2.rend());//逆序下
	string str=str2+'$'+s;
	information info=fun_height(str);
	int len=str.size();
	int max=-0xffff,i;//最长子回文的长度  其实求的是 height[i] 的最大值 且 sa[i] sa[i-1] 一个在0--->(len-1)/2-1  另一个在  (len+1)/2-->len-1
	int start,start2;//记下回文的开始位置
	for(i=1;i<len;i++)
	{
		//cout<<i<<"   "<<info.height[i]<<endl;
		
		
		if(info.height[i]>max&&isok(info.sa,i,len))
		{
			//cout<<str.substr(info.sa[i-1],len)<<"   "<<str.substr(info.sa[i],len)<<endl;
			max=info.height[i];
			start=info.sa[i];
			start2=info.sa[i-1];
			if(start2<start)
				start=start2;

			
		}
	}
	/*打印回文*/
	cout<<"  max ="<<max<<endl;
	cout<<str.substr(start,max)<<endl;

}
子串的个数 问题:

每个子串一定是某个后缀的前缀,那么原问题等价于求所有后缀之间的不相同的前缀的个数。如果所有的后缀按照suffix(sa[1]), suffix(sa[2]),suffix(sa[3]), …… ,suffix(sa[n])的顺序计算,不难发现,对于每一次新加进来的后缀suffix(sa[k]),它将产生n-sa[k]+1 个新的前缀。但是其中有height[k]个是和前面的字符串的前缀是相同的。所以suffix(sa[k])将“贡献”出n-sa[k]+1- height[k]个不同的子串。累加后便是原问题的答案。这个做法
的时间复杂度为O(n)。

代码:

int get_cnt(string str)
{
	information info=fun_height(str);
	int len=str.size();
	/*对height数组  当加入height[i]的时候,增加的子串的个数为 len-1-sa[i]+1-height[i] len-1下标从0开始的*/
	int sum=len-1-info.height[0]+1;
	for(int i=1;i<len;i++)
	{
		sum+=len-1-info.sa[i]+1-info.height[i];
	}
	return sum;

}

重复子串:字符串R 在字符串L 中至少出现两次,则称R 是L 的重复子串

可重叠最长重复子串问题:给定一个字符串,求最长重复子串,这两个子串可以重叠。

算法分析:
这道题是后缀数组的一个简单应用。做法比较简单,只需要求height 数组里的最大值即可。首先求最长重复子串,等价于求两个后缀的最长公共前缀的最大值。因为任意两个后缀的最长公共前缀都是height 数组里某一段的最小值,那么这个值一定不大于height 数组里的最大值。所以最长重复子串的长度就是
height 数组里的最大值。这个做法的时间复杂度为O(n)

代码:

/*取最长重复子串  这个2个子串可以重复*/
void get_longest(string str)
{
	information info=fun_height(str);
	int len=str.size();
	int max=-0xffff,i,index,index2;
	for(i=0;i<len;i++)
	{
		if(max<info.height[i])
		{
			
			max=info.height[i];
			if(max==0)
			{
			index=info.sa[0];
			}else
			{
				index=info.sa[i-1];
				index2=info.sa[i];
			}
			

		}
	}
	/*打印出结果*/
	if(max==0) return;
	else
	{
		cout<<"max ="<<max<<endl;
		cout<<"substr1  ="<<str.substr(index,len)<<endl;
		cout<<"substr2  ="<<str.substr(index2,len)<<endl;
	}

}
不可重叠最长重复子串(pku1743)
给定一个字符串,求最长重复子串,这两个子串不能重叠

这题比上一题稍复杂一点。先二分答案,把题目变成判定性问题:判断是否存在两个长度为k 的子串是相同的,且不重叠。解决这个问题的关键还是利用
height 数组。把排序后的后缀分成若干组,其中每组的后缀之间的height 值都不小于k

/*取最长重复子串  这个2个子串不可以重复*/
void get_longest2(string str)
{
	information info=fun_height(str);
	int len=str.size();
	int k=len/2,i,start,end;
	int ii,max,min;//height[start]--->height[end] 中最大的sa[]用max  最小的sa[]的用min
	bool flag=false;//一组的开始标志
	/*假设最长不重叠的重复子串为k */
	for(;;k--)
	{
		/*对 height 按长度k  分组*/
		for(i=0;i<len;i++)
		{
			if(!flag&&info.height[i]>=k)
			{
				flag=true;
				start=end=i;
			}else if(flag&&info.height[i]<k)
			{
				max=-0xffff,min=0xffff;
				for(ii=start-1;ii<=end;ii++)
				{
					if(info.sa[ii]>max)
						max=info.sa[ii];
					if(info.sa[ii]<min)
						min=info.sa[ii];
					if(max-min>=k)
					{
						cout<<"最大不重复的 "<<k<<endl;
						cout<<str.substr(min,len)<<endl;
						cout<<str.substr(max,len)<<endl;
						return;
					}
					flag=false;
				}
			}else
			{
				end=i;
			}
		}

	}

}

还有好的例子,后缀数组的计算和height数组的计算 时间复杂度为 nlogn









  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值