给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
别人想法:
(1)
一个字符的串肯定是一个回文串;
两个字符只要相同,它们在一起也构成一个回文串;
三个字符的串,只要首尾两个字符相同,那它也是一个回文串;
以此类推,如果一个字符串,处于它的首尾两个字符之间的子串是回文串,并且它的首尾字符相同,则它就是一个回文串。
总结,当一个串有三个字符时,我们可以用中间一个字符的情况加上它的首尾字符是否相同推出来;当一个串有四个字符时,我们可以用中间两个字符的情况加上它的首尾字符是否相同推出来,按照这个规律递推下去,就能判断所有长度的子串(包括相同长度的不同子串)是否是回文串。
因此,我们需要一个二维数组 bool dp[len][len] 来缓存一个串是否是回文串的结果,第一维是字符串的首字符的下标,第二维是字符串的尾字符的下标,用 dp[i][j] 的值表示str[i…j]这个串是否是回文串,true代表是,false代表否。这样就可以缓存每个串是否是回文串的结果,当我们枚举所有串来判断它们是否是回文串时,就可以借助缓存的结果(当前串去掉首尾字符的子串是不是回文串),用O(1)的时间复杂度快速地对当前串做出判断。
我们定义:
dp[i][j] = true(当串str[i…j]是回文串)
dp[i][j] = false(当串str[i…j]不是回文串)
并且可以得到以下的状态转换方程:
dp[i][j] = (dp[i + 1][j - 1] && str[i] == str[j])
具体解法如下:
当串长度为以 1 时,dp[i, i] = true(0 <= i < len);
当串长度为 2 时,dp[i, i+1] = (str[i] == str[i+1]) (0 <= i < len - 1),前两种情况很简单,我们直接判断并初始化dp数组;
当串长度大于等于3时,我们使用状态转换方程进行递推。
动态规划的过程如下图:
(2)
假设任意给你一个串,例如:aasbavvaba那么串长len可以划分:1)len = 1; 2)len = 2; 3)len > 2;这样划分的理由是:任何串内最短的回文串长len>=1,这点毋庸置疑,而划分长度为2是因为,但凡一个子串长度len大于2都是在长度为1或2的基础上增加的。判断规则:每次枚举一个小于等于len的长度i然后判断下标j与下标j+i所对应的字符是否相等,不等怎j向后枚举,若想等,此时需要判断从j+1到j+i-1是否为回文串,是则更新不是则j向后继续枚举。这样就建立起一个关系,也就是当前状态只和上一个状态有关,若上一个状态是回文串(这里代码中用dp数组保存状态为1表示上一个状态为回文串,否则不是),此时只需判断s[i]与s[j+i]是否相等即可。
[注意]:dp[i][j]表示从下标i到j为会问串,因此i<=j,这里就要避免i>j的情况,因为若枚举两个相邻的字符时,那么dp[j+1][j+i-1]中j+1 > j+i-1,因此我们自己手动将dp[j+1][j]初始化为1,表示这种情况是正常的即可。
自己想法:
首先想到动态规划定义,将这个大问题分解成一个个的小问题的解,而这些小问题的解之间还有联系(先求出1/2子串,根据这两个子串再向上查找最长子串),最后将小问题的解汇总出最终解。
这道题精髓就在设计那个二维数组,横坐标长度是s.size(),纵坐标长度也是s.size()。横纵坐标可以想象是两个游标,横坐标是第一个游标(判断最长子串的起点),纵坐标是第二个游标(判断最长子串的终点),之后组成一个二维矩阵,之后根据这个矩阵和首尾字符串是否相等,来判断是否更新最长字符串。具体解析在代码注释。
#include<vector>
class Solution {
public:
string longestPalindrome(string s) {
int s_length = s.size();
if (s_length ==0 || s_length ==1)
{
return s;
}
int max = 1;
int start = 0;
vector<vector<int>>bp(s_length,vector<int>(s_length));//需要复习二维数组初始化
//下面这个for循环是初始化bp数组,也是找出基础的1、2最长子串,为下面的for循环做准备,因为以后长度的子串都是以1、2长度子串做基础
for (int i = 0; i < s_length;i++)
{
bp[i][i] = 1;//这个一开始错了,应该是1,这个是当长度为1时,二维矩阵对角线为1;
if (i < s_length-1 && s[i] == s[i+1])
{
bp[i][i+1] = 1;
max = 2;
start = i;
}
}
//下面这个循环,第一个for循环是最长子串长度递增,第一个是长度为3的子串,第二个for循环就是游标递增,
//长度为3时,则首游标就是先从0开始,尾游标就是2(3+0-1),然后首游标k递增(1、2、3...)往后寻找长度为3子串
//下面的if循环就是判断,因为上面初始化时有最小2个最长子串,所以判断长度为3子串时候时,只需要
//长度为2的子串bp数组 == 1 和首尾字符串相等,就可以推出长度为3的字符串,这也就是动态规划的含义所在,
//求更长子串,需要短的子串做基础。
//而start和max的意义就是最后知道从哪裁剪字符串
for (int l = 3; l <= s_length; l++)//这块错了,这应该是<=,因为l是最长长度,是可以等于s.length()的
{
for (int k = 0; l + k - 1< s_length; k++)//这块l + k - 1< s_length判别条件自己一开始写错了。注意应该是后面的游标<s_length
{
int m = l + k - 1;
if (bp[k+1][m-1] == 1 && s[k] == s[m])
{
max = l;
start = k;
bp[k][m] = 1;
}
}
}
return s.substr(start, max);
}
};
还有一种想法:中心扩算法,时间不够没看代码,把思想消化一下,有时间再看(官网解答)
我们观察到回文中心的两侧互为镜像。因此,回文可以从它的中心展开,并且只有 2n - 1 个这样的中心。
你可能会问,为什么会是 2n - 1 个,而不是 n 个中心?
因为回文的中心要区分单双。
假如回文的中心为 双数,例如 abba,那么可以划分为 ab bb ba,对于n长度的字符串,这样的划分有 n-1 种。
假为回文的中心为 单数,例如 abcd, 那么可以划分为 a b c d, 对于n长度的字符串,这样的划分有 n 种。
对于 n 长度的字符串,我们其实不知道它的回文串中心倒底是单数还是双数,所以我们要对这两种情况都做遍历,也就是 n+(n-1)= 2n - 1,所以时间复杂度为 O(n)。
当中心确定后,我们要围绕这个中心来扩展回文,那么最长的回文可能是整个字符串,所以时间复杂度为 O(n)。
所以总时间复杂度为 O(n^2)
string longestPalindrome(string s)
{
if (s.length() < 1)
{
return "";
}
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++)
{
int len1 = expandAroundCenter(s, i, i);//一个元素为中心
int len2 = expandAroundCenter(s, i, i + 1);//两个元素为中心
int len = max(len1, len2);
if (len > end - start)
{
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substr(start, end - start + 1);
}
int expandAroundCenter(string s, int left, int right)
{
int L = left, R = right;
while (L >= 0 && R < s.length() && s[L] == s[R])
{// 计算以left和right为中心的回文串长度
L--;
R++;
}
return R - L - 1;
}
参考文章:
https://blog.csdn.net/CSDN_FengXingwei/article/details/82429808
https://blog.csdn.net/small__snail__5/article/details/80310615