思路来源:
《动态规划-最长回文字符串》
《【一题六解】🤣从暴力、区间dp到字符串哈希,掌握解决该类问题通法》
《最长回文子串 C++》
https://leetcode.cn/problems/longest-palindromic-substring/solution/-by-coco-e1-qjns/
1.题目说明
2.分析思路
3.测试
4.总结
1.题目说明:
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
分析思路:
最开始思路如下:
1.使用暴力求解:
可以令start作为字符串的首部 ,end作为字符串的尾部,然后遍历一遍原字符串,然后在遍历过程中设置一个检测函数检测是否为回文字符串,如果是回文字符串则将它的长度记录下来,这样经过程序的遍历最大长度的回文字符串将被保留并且返回。但是在leetcode的在线测试网站上面无法通过,在第35个测试案例中会多输出一个字符,这个问题我无法解决,以下是根据原java代码改动的c++代码
#include<iostream>
#include<string>
using namespace std;
class solution
{
public:
string longestPalindrome2(string s)
{
int n = s.length();
int maxlen = 1;
int left = 0;
for (int len = 2; len <= n; len++)//划分子字符串的长度
{
//首先确定子字符串的长度len,然后通过改变start大小来得到不同的子字符串,一直遍历到原字符串结束
for (int i = 0; i+len <= n; i++)
{
int j = i + len - 1;
if(check(s,i,j))
{
if (maxlen<len)
{
left = i;
maxlen = len;
}
}
}
}
return s.substr(left, left + maxlen);
}
private:
bool check(string s, int i, int j)
{
while (i<j)
{
if (s[i] != s[j])return false;
i++;
j--;
}
return true;
}
};
根据发表官网的帖子其中的暴力解法一:
其思路是将字符串反转然后比对,,即将字符串划分为不同start的子字符串,然后将其中的子字符串挨个反转比较,可以看出效率十分低下,依旧无法通过测试(超时).
class Solution {
public:
string longestPalindrome(string s) {
string res="";//存放结果
string temp="";//存放子串
for(int i=0;i<s.length();i++)
{
for(int j=i;j<s.length();j++)//为啥要第二层循环?原因是从不同start开始遍历
{
temp=temp+s[j];
string tem=temp;//tem存放子串反转结果
std::reverse(tem.begin(),tem.end());//反转
if(temp==tem)
res=res.length()>temp.length()?res:temp;
}
temp="";
}
return res;
}
};
同帖的解法二,还是同样的思路,但是不同的是寻找原字符串的反转字符串的最长公共子字符串,然后判断这个最长公共子字符串是不是回文的,其步骤为将原序列反转得到反转序列rev,然后循环查找最长公共子序列,找到了查看是不是回文字符串,如果最后都没有找到就这个算法就拉倒了。
class Solution {
public:
string longestPalindrome(string s) {
if(s.length()==1) return s;//大小为1的字符串必为回文串
string rev=s;//rev存放s反转结果
string res;//存放结果
std::reverse(rev.begin(),rev.end());
if(rev==s) return s;
int len=0;//存放回文子串的长度
for(int i=0;i<s.length();i++)//查找s与rev的最长公共子串
{
string temp;//存放待验证子串
for(int j=i;j<s.length();j++)
{
temp=temp+s[j];
if(len>=temp.length())
continue;
else if(rev.find(temp)!=-1)//在rev中找到temp
{
string q=temp;//q用来验证temp是否是回文子串
std::reverse(q.begin(),q.end());
if(q==temp)
{
len=temp.length();
res=temp;
}
}
else break;
}
temp="";
}
return res;
}
};
这里有一个问题,暴力方法二中的最长公共子序列也可以使用动态规划来求解,我们是否可以将其他的最长公共子序列使用动态规划的方法求解以此提高算法的效率呢?
2.动态规划
2.1 动态规划思想简介:
将问题划分为子问题,保存已解决子问题的答案,需要时再找出已求解的答案,避免重复计算。
因为我对动态规划并不熟悉,因此这里直接粘贴leetcode官网的解答:
思路与算法
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果我们已经知道 “bab” 是回文串,那么 “ababa” 一定是回文串,这是因为它的首尾两个字母都是 “a”。
根据这样的思路,我们就可以用动态规划的方法解决本题。我们用 P(i,j)P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串(下文表示成 s[i:j]s[i:j])是否为回文串:
这里的「」包含两种可能性:
1. 本身不是一个回文串;
2.,此时 s[i, j]s[i,j] 本身不合法。
那么我们就可以写出动态规划的状态转移方程:
也就是说,只有 是回文串,并且
的第
和
个字母相同时,
才会是回文串。
上文的所有讨论是建立在子串长度大于 2 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 1或 2。对于长度为 1 的子串,它显然是个回文串;对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
根据这个思路,我们就可以完成动态规划了,最终的答案即为所有 中
(即子串长度)的最大值。注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
vector<vector<int>> dp(n, vector<int>(n));
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= n; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < n; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= n) {
break;
}
if (s[i] != s[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
//j-i+1的大小等值于前面的L,也可以使用L替换j-i+1
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substr(begin, maxLen);
}
};
其中有一个问题就是为什么在动态规划中的下表加一可以通过测试,但是在上面的暴力解法一中却是错误的?