本文参考了 后缀数组--处理字符串的有力工具
子串:字符串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