目录
前言
在字符串处理中,求最长重复子串是一个经典问题,结合了哈希+二分的优化算法。
一、最长重复子串
题目描述:

重复子串:即在字符串中至少出现两次的子串,输入输出如下所示:

约束条件:

1、暴力枚举
最直接的想法就是暴力枚举,两层for循环枚举所有可能的子串,检查它是否重复出现,代码如下所示:
cpp
class Solution
{
public:
int longestRepeatingSubstring(string s)
{
int n=s.size();
for(int len=n-1;len>=0;len--)
{
for(int i=0;i+len<=n;i++)
{
string str=s.substr(i,len);
if(s.find(str,i+1)!=string::npos)
{
return len;
}
}
}
return 0;
}
};
外层循环从n-1到0枚举长度,找到第一个满足条件的就直接返回,保证是最长长度。内层循环i枚举所有起始位置,string str=s.substr(i,len),用substr取子串。s.find(str,i+1)从i+1位置开始查找子串,if(s.find(str,i+1)!=string::npos),表明str为重复子串,len为最长长度,return len。
时间复杂度分析:
枚举子串数量:O(n^2)
find:O(n)
总复杂度:O(n^3)
显然O(n^3)在1<=n<=2000这个数据范围内不可过,需对该算法进行优化。
2、哈希+二分
核心观察:如果长度为L的重复子串存在,那么长度为L-1的重复子串也一定存在,只要截取前L-1个字符即可,因此子串长度具有单调性,可以考虑进行二分查找。二分判断的依据通过设计一个哈希函数,计算所有长度为L的子串,存入哈希集合,如果某个子串重复出现,返回true。代码实现如下:
cpp
class Solution {
public:
int longestRepeatingSubstring(string s) {
int n=s.size();
int left=0,right=n-1;
int ans=0;
while(left<=right)
{
int mid=left+(right-left)/2;
if(hashst(s,mid))
{
ans=mid;
left=mid+1;
}
else
{
right=mid-1;
}
}
return ans;
}
private:
bool hashst(string& s,int len)
{
if(len==0) return true;
unordered_set<string> seen;
for(int i=0;i+len<=s.size();i++)
{
string str=s.substr(i,len);
if(seen.count(str)) return true;
seen.insert(str);
}
return false;
}
};
1、二分枚举长度:
在0,n-1范围内二分查找最长重复子串的长度,int mid=left+(right-left)/2,取中间值mid,调用hashst函数验证该长度下是否存在重复子串。
2、hashst函数:
unordered_set<string> seen,string str=s.substr(i,len),seen.insert(str),hashst函数内部通过for循环来存储所有len长度的子串。if(seen.count(str)),一旦发现某个子串str已经存在,return true,说明该长度可行。
3、二分决策:
if(hashst(s,mid)),如果当前长度可行,ans=mid,left=mid+1,则尝试更长的长度,else则right=mid-1,不可行则缩短长度继续尝试,最终ans即为最长重复子串的长度。
时间复杂度分析:
二分查找时间复杂度为O(logN),hashst函数内部遍历字符串中所有长度为mid的子串,为O(N),因此总时间复杂度为O(NlogN)。
小结
本道题是字符串处理的经典题,通过观察重复子串长度具有单调性,从而使用二分进行查找,二分判断通过哈希来存储所有长度为len的子串,一旦遇到重复就返回真,说明该长度可行,然后二分继续尝试更长的长度,如果不可行则缩短长度继续尝试,通过二分+哈希优化实现了O(NlogN)的查找效率,是字符串处理的常用解法。