最长重复子串问题
假设我们有一个字符串,要如何求出在这个字符串里最长的重复子串呢?比如给出字符串abcdeefabcd,显然这个字符串的重复子串有很多,比如a、b、ab、abc 等等,那么通过观察,我们可以知道这个字符串的最长重复子串(Longest Repeated Substring)为abcd。
如果要用编程的方法来找出这个最长重复子串,应该怎么做呢?
暴力遍历
非常正常的想法是遍历,利用三重循环,每一次比较两个相等长度的子字符串是否相等,将记录下的最大长度与该子字符串进行比较,如果更长,则更新答案。简单的代码如下:
#include<iostream>
#include<string.h>
using namespace std;
string naiveLRS(string s)
{
int maxm=0; //重复子字符串的最大长度
string ans = "";
for(int i=0;i<s.length();i++)
{
for(int j=i;j<s.length();j++)
{
string x=s.substr(i,j-i+1); //一个子字符串
for(int k=i+1;k<s.length();k++) //从i+1开始,因为如果从i开始,两字符串的起点相同,得到的答案必定为原字符串s
{
string y=s.substr(k,j-i+1); //另一个与x不相等的子字符串
if(x == y && x.length() > maxm) //如果这两个字符串相等,并且长度比之前记录的maxm更大,则更新答案
{
maxm = x.length();
ans = x;
}
}
}
}
return ans;
}
int main()
{
string s,ans;
cout << "Enter the input string:-\n";
cin >> s; //输入字符串
ans = naiveLRS(s);
cout << "The longest repeating substring is:- " << ans << '\n'; //输出答案
return 0;
}
显然,这种方法的时间复杂度是 O ( n 3 ) O(n^3) O(n3),空间复杂度为 O ( 1 ) O(1) O(1)。值得一提的是,在第三层循环中,是不需要考虑如果长度 j − i + 1 j-i+1 j−i+1超出字符串长度的情况,因为在substr()函数中,如果给出的长度超出了字符串原本的长度,会自动截取到字符串的结尾。
动态规划
显然,前面提到的暴力解法涉及到了非常多的重复计算。而动态规划的思想,实际上就是利用“记忆”下来的数据,避免这些重复计算。这些“记忆”下来的数据,大概可以理解成“历史记录”。我们一般会用一个数组dp来存放这些“历史记录”。问题是,这个数组的意义是什么,里面的值应该怎样更新呢?
LRS问题中dp数组的意义
首先呢,我们需要明确这个dp数组的意义。在我们提到的最长重复子串问题中,dp[i][j]存储的是以第i和第j个字符结尾的重复子字符串的长度。这样表述还是有点抽象,以一个具体的字符串为例子,如banana。我们会将dp数组初始化如下:
b | a | n | a | n | a | ||
---|---|---|---|---|---|---|---|
i \ j | 0 | 1 | 2 | 3 | 4 | 5 | |
b | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
a | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
n | 2 | 0 | 0 | 0 | 0 | 0 | 0 |
a | 3 | 0 | 0 | 0 | 0 | 0 | 0 |
n | 4 | 0 | 0 | 0 | 0 | 0 | 0 |
a | 5 | 0 | 0 | 0 | 0 | 0 | 0 |
事实上就是一个n*n的矩阵(n为字符串的长度),初始化为0。那么我们从i开始遍历。起初 i = 0 i=0 i=0,那么开始遍历 j j j,为了避免得到两个完全重合的子字符串, j j j的初始值为 i + 1 i+1 i+1,也就是1,随后我们比较 s [ i ] s[i]