题目描述:
求一个串中出现的第一个最长重复子串
采用顺序结构存储串,编写一个程序,求串s中出现的第一个最长重复子串的下标和长度。
求解思路:
一、 主要数据结构及涉及知识点
核心结构:后缀数组
其他涉及:string 数据类型、 vector 容器 [ 顺序存储 ] 、 map<key,value> 容器、 STL 泛型算法 stable_sort
二、 算法的基本思想描述和流程框图
算法基本思想描述
首先,题目有一点小问题,没有说明最长重复字串是否可重叠,此处假设为可重叠,所以,问题归纳为:求一个串中出现的第一个最长可重叠子串。
一般解法:动态规划
但是用这一方法解此类问题,时间复杂度很难控制到O(N*logN) 以内。所以,求解此类问题,用后缀数组更为合适。
更优解法:后缀数组
那么何为后缀数组, 罗穗骞 在其 国家集训队论文《 后缀数组----处理字符串的有力工具 》一文中如下定义:
后缀数组是处理字符串的有力工具。后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也并不逊色,而且它比后缀树所占用的内存空间小很多。
具体描述如下:
假设一个字符串s="ababa",其后缀共有如下五个:
1、 "ababa"
2、 "baba"
3、 "aba"
4、 "ba"
5、 "a"
注意:此时需用map<string,int>容器定义一个变量rank,存储后缀字符在串s中开始的位置,
例如:rank["ababa"]=0;
而刚刚所讲的后缀数组则是存储经过按“字典序”排序后的字符串s的后缀,即:
定义一个后缀数组[此处用C++容器代替C数组]:vector<string> sa; //sa:Suffix Array
则经过sa.push_back()添加操作后,将排序后的s的后缀存放如下所示:
sa[0]="a";
sa[1]="aba";
sa[2]="ababa";
sa[3]="ba";
sa[4]="baba";
最后,再通过vector<string>::iterator迭代遍历,将两个相邻位置的后缀数组比较,取其最长公共前缀。例如:
Sa[0]和sa[1]的最长公共前缀为"a";
sa[1]和sa[2]的最长公共前缀为"aba";
Sa[2]和sa[3]的最长公共前缀为空;
Sa[3]和sa[4]的最长公共前缀为"ba";
综上,得出最长公共前缀为"aba",即所求字符串s的最长可重叠重复子串。
可调用以下函数比较并返回最长公共前缀结束下标:
int MaxPrefix(ITER s1,ITER s2)
{
int i=0;
while(!(*s1).empty()&&(*s1)[i]==(*s2)[i]) ++i;
return i;
}
在主函数中如下循环,将返回的最大下标值保存至max:
max=-1;
for(iter=sa.begin();iter!=sa.end()-1;++iter)//O(N)
{
temp=MaxPrefix(iter,iter+1);
if(temp>max)
{
max=temp;
maxi=iter;
}
}
求得的max即为最长重复子串的长度,调用一下语句输出结果:
for(int i=0;i<max;++i)
maxt+=(*maxi)[i];
cout<<"长度为:"<<max<<endl;//输出所有字符串长度
cout<<"下标为:"<<rank[maxt]<<endl;//利用之前保存的键-值对应关系,输出起始下标
cout<<maxt<<endl;//输出所求字符串
最后再分析一下整个算法的时间复杂度:
for(size_t i=0;i<s.size();++i) {...}//O(N)
stable_sort(sa.begin(),sa.end(),comp);//O(N*logN)
for(iter=sa.begin();iter!=sa.end()-1;++iter)//O(N)
O(N)+O(N*logN)+O(N)=O(N*logN)
所以,求的最后的时间复杂度为O(N*logN).
三、 源代码
//求一个字符串的可重叠最长重复子串——后缀数组解法
//求一个字符串的可重叠最长重复子串——后缀数组解法 #include <iostream> #include <string> #include <algorithm> #include <vector> #include <map> using namespace std; typedef vector<string>::iterator ITER; bool comp(string s1,string s2) { return s1<s2; } int MaxPrefix(ITER s1,ITER s2) { int i=0; while(!(*s1).empty()&&(*s1)[i]==(*s2)[i]) ++i; return i; } int main() { int temp,max; string s,t,maxt; vector<string> sa; map<string,int> rank; //sa:Suffix Array 后缀数组,此处用vector容器代替 cin>>s; for(size_t i=0;i<s.size();++i) {//O(N) t=s.substr(i,s.size()); sa.push_back(t); rank.insert(pair<string,int>(t,i)); } stable_sort(sa.begin(),sa.end(),comp);//O(N*logN) ITER iter,maxi; max=-1; for(iter=sa.begin();iter!=sa.end()-1;++iter)//O(N) { temp=MaxPrefix(iter,iter+1); if(temp>max) { max=temp; maxi=iter; } } for(int i=0;i<max;++i) maxt+=(*maxi)[i]; cout<<"长度为:"<<max<<endl; cout<<"下标为:"<<rank[maxt]<<endl; cout<<maxt<<endl; return 0; }
四、 测试数据与结果
ababa
长度为:3
下标为:2
aba
请按任意键继续. . .
hhhhh
长度为:4
下标为:1
hhhh
请按任意键继续. . .
五、 调试及算法分析与改进
关于该后缀数据解法的时间复杂度已经在之前的算法的基本思想中得出:O(N*logN)
那么,如何再对其进行优化,使得时间复杂度更小呢,我们看代码可以知道,要对整个算法进行优化,最大的突破点就是那个C++ 泛型排序算法 stable_sort() ,它的时间复杂度在 O(N*logN)~O(N*log(N^2) 之间,而我们知道在排序算法中,其中基数排序的时间复杂度为 O(d(n+r)),r 为基数, d 为关键字位数 , 且较为稳定。
所以,如果换成基数排序的话,有可能时间复杂度还可以小一点。
六、 总结或心得体会
以后做题,要学会从多个方面考虑问题,不要被固有的思维所限制,试着逐步优化算法,并能够在此基础上创新出新的更高效的解决方案。