后缀数组的应用

来源:互联网 发布:fbp算法源代码及注释 编辑:程序博客网 时间:2024/05/17 22:57

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

子串:字符串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 255int 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;elser[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--;}elsebreak;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--;}elsebreak;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-1int 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[]的用minbool 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
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 鞋子穿久了会有臭味怎么办 考研忘了自己填写的通讯地址怎么办 基础教育教师培训网注册错了怎么办 11个月宝宝便秘大便有血怎么办 两岁宝宝两天没拉粑粑了怎么办 两岁宝宝两天没拉大便怎么办 两岁宝宝便秘两天没拉怎么办 昨天带孩子上早教课一直哭怎么办 网页账号注册无法获取验证码怎么办 我总是为一些事情而烦恼怎么办 手机键盘打出的字是繁体字怎么办 阴阳师协助任务对方接了不做怎么办 wow牧师选错了圣物怎么办 淘宝有几个订单被管控了怎么办 埋线双眼皮一个宽一个窄怎么办 把维D和维C一起吃了怎么办 微信出现当前登录环境异常该怎么办 去麦加朝觐人要出现死亡怎么办 炉石传说偶数骑削弱后怎么办 出于安全和性能方面的原因怎么办 怀孕后不知道拍过好几次牙片怎么办 百度云种子含有非法离线内容怎么办 苹果手机上的迅雷怎么打不开怎么办 微信绑定银行卡收不到验证码怎么办 支付宝转账为什么没验证码怎么办 电视时间比手机慢三分钟怎么办 13岁现在有月经又流鼻血怎么办 酒店给客人把房间开错了怎么办 手机百度云下载的视频分解了怎么办 知道家人被绑架北派传销怎么办 接手前同事的客人无回复怎么办 喝了酒第二天一直吐怎么办 孩子看的鬼片不敢睡觉害怕怎么办 宝宝满月了脸还有点黄怎么办 我情人天天说没时间出去该怎么办 两个月宝宝下体三角区肿了怎么办 夏养的龙猫家里有味怎么办 不小心踩到仓鼠吐血了怎么办 不小心踩到仓鼠的头怎么办 母仓鼠生完孩子特别暴躁怎么办 被仓鼠咬出血怎么办要不要打针