1、题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba”也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
2、代码实现
2.1 C++语言
2.1.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时,我们使用状态转换方程进行递推。
动态规划的过程如下图:
动态规划的第一种代码
class Solution {
public:
//动态规划,O(n^2)
//根据len大小创建dp数组,涉及到创建、初始化和销毁的过程,必然是消耗时间节省空间
//如果牺牲空间节省时间,可以这样定义二维数组: bool dp[1000][1000] = {false};
string longestPalindrome(string s)
{
int start = 0; //最大回文子串起始地址
int maxSubLen = 0; //最大回文子串长度
int subLen = 0; //当前回文子串长度
const int len = s.length(); //母串长度
//创建二维数组dp
//bool(*dp)[len] = new bool[len][len];//不行,要求len是常量
bool **dp = new bool *[len];//指针数组,包含len个bool指针
for (int i = 0; i < len; ++i)
{
dp[i] = new bool[len];//每个bool指针指向一个包含len个bool元素的一维数组
}
//初始化二维数组dp
for (int i = 0; i < len; ++i)
{
for (int j = 0; j < len; ++j)
{
dp[i][j] = false;
}
}
// 子串长度为1和为2的初始化
for (int i = 0; i < len; ++i)
{
dp[i][i] = true;
subLen = 1;
if (subLen >= maxSubLen)
{
start = i;
maxSubLen = subLen;
}
if (i < len - 1 && s.at(i) == s.at(i + 1))
{
dp[i][i + 1] = true;
subLen = 2;
if (subLen >= maxSubLen)
{
start = i;
maxSubLen = subLen;
}
}
}
// 使用上述结果可以dp出子串长度为3~len的子串
for (int strlen = 3; strlen <= len; ++strlen) //子串长度
{
for (int i = 0; i <= len - strlen; ++i) //子串起始地址
{
int j = i + strlen - 1; // 子串结束地址
if (dp[i + 1][j - 1] && s.at(i) == s.at(j))
{
dp[i][j] = true;
start = i;
maxSubLen = strlen;
}
}
}
//销毁二维数组dp
for (int i = 0; i < len; ++i)
{
delete[] dp[i];
}
delete[]dp;
//返回最长回文子串
return s.substr(start, maxSubLen);
}
};
动态规划的第二种代码
class Solution {
public:
//动态规划O(n^2)
//二维数组dp的第一维是子串中首字符下标,第二维是尾字符下标
//dp[i][j]代表以i,j为起止下标的子串是否是回文串,true代表是,false代表否
//如果dp[i+1][j-1]是true,且str[i]=str[j],则dp[i][j]也是true
//用dp[i][j]缓存str[i...j]是否是回文串,可以在枚举所有子串的时候,根据之前缓存的结果快速判断当前子串是否是回文串
string longestPalindrome(string s)
{
int len = s.length();//母串长度
int start = 0, end = 0;//最长回文子串的起止地址
//创建二维数组dp,用于动态规划
bool **dp = new bool *[len];
for (int i = 0; i < len; ++i)
{
dp[i] = new bool[len];
}
//初始化二维数组dp,若不初始化,默认初始化为true
for (int i = 0; i < len; ++i)
{
for (int j = 0; j < len; ++j)
{
dp[i][j] = false;
}
}
//查找最长回文子串
for (int i = 0; i < len; ++i)//子串首尾字符的间距,最小是0,最大是len - 1
{
for (int j = 0; j < len - i; ++j)//j + i是子串尾字符下标,要小于len,不能越界
{
//对子串分情况讨论
if (i == 0)//子串长度为1
{
dp[j][j + i] = true;
}
else if (i == 1)//子串长度为2
{
if (s.at(j) == s.at(j + i))
{
dp[j][j + i] = true;
}
}
else//子串长度为3~len,此时大子串去掉首尾字符后还是小子串
{
if (dp[j + 1][j + i - 1] && s.at(j) == s.at(j + i))
{
dp[j][j + i] = true;
}
}
//子串str[j...j+i]是回文串
//最长回文子串一定是所有回文子串中最后被枚举到的
if (dp[j][j + i] == true)
{
start = j;
end = j + i;
}
}
}
//销毁二维数组dp
for (int i = 0; i < len; ++i)
{
delete[]dp[i];
}
delete[]dp;
//返回最长回文子串
return s.substr(start, end - start + 1);
}
};
2.1.2 Manacher算法(马拉车算法)
参考文档:【回文串-Manacher】
class Solution {
public:
//O(n)
string longestPalindrome(string s)
{
//s为空直接返回
if (s.empty())
{
return s;
}
//加入特殊字符,前:babab,后:(#b#a#b#a#b#)
string tmp = s;
int len = tmp.length();
for (int i = 0, j = 0; i <= len; ++i)
{
tmp.insert(j, "#");
j = j + 2;
}
tmp.insert(0, "(");//防止越界
tmp.push_back(')');
//创建并求解辅助数组rad[]
//rad[i]是以s[i]为中心字符的最长回文子串的半径,即它的最右字符和中心字符的下标之差
len = len * 2 + 3;
int *rad = new int[len];
//i用来遍历tmp串,并有k跳跃;j是回文半径,k是从1到rad[i]的值,用来求rad[i+1]到rad[i+rad[i]]的值
for (int i = 1, j = 0, k; i < len - 1; i += k)
{
//判断以 tmp[i] 为中心,j+1 为半径的子串是否是回文串
while (tmp.at(i - j - 1) == tmp.at(i + j + 1))
++j;//回文半径加 1,继续判断
rad[i] = j;//找到以s[i]为中心的最大回文子串,用回文半径j初始化rad[i]
//镜像,遇到rad[i-k]=rad[i]-k停止,这时不用从j=1开始比较
for (k = 1; k <= rad[i] && rad[i - k] != rad[i] - k; ++k)
rad[i + k] = min(rad[i - k], rad[i] - k);
j = max(j - k, 0);//更新j
}
int maxLen = 0;//最大回文子串的长度
int start;//最大回文子串的起始地址
//遍历rad数组找到最大回文子串的长度和起始地址
for (int i = 1; i < len - 1; ++i)
{
if (rad[i] > maxLen)
{
maxLen = rad[i];
start = (i - maxLen + 1) / 2 - 1;
}
}
return s.substr(start, maxLen);//返回最大回文子串
}
};