一、题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
二、思路及代码实现
思路一:中心扩展法
若回文串的长度为奇数,则回文串的中心位置是字符;若回文串的长度为偶数,则回文串的中心位置位于两个字符的中间。所以,我们遍历字符串,每次选择一个中心,进行左右扩展,找到所有的回文串并记录最长回文串。
参考代码:
class Solution {
public String longestPalindrome(String s) {
if(s.length() == 0 || s == null)
return "";
int left = 0;// 回文串的左边界
int right = 0;// 回文串的右边界
for(int i = 0; i < s.length(); i++){
int odd = expandAroundCenter(s, i, i);// 奇数长度的回文串
int even = expandAroundCenter(s, i, i + 1);// 偶数长度的回文串
// 更新每个回文串的left和right,记录最长回文串的长度
int maxLength = Math.max(odd, even);
if(maxLength > right - left){
left = i - (maxLength - 1) / 2;
right = i + maxLength / 2;
}
}
return s.substring(left, right + 1);
}
// 以left和right为中心向两边扩展;
// 初始left = right时,找到奇数长度的回文串;初始left = i,right = i + 1时,找到偶数长度的回文串。
// 最后返回回文串的长度
public int expandAroundCenter(String s, int left, int right){
int l = left;
int r = right;
while(l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)){
l--;
r++;
}
return r - l - 1;
}
}
思路二:动态规划
动态规划的核心在于记录子问题的状态,省掉重复计算。所以我们可以在中心扩展法的基础上,使用动态规划来解决问题。
- 问题拆解:
对于一个回文串,两边同时缩短,得到的也是回文串。如,“abdba”,去掉两边的 ‘a’ 之后得到 “bdb” 还是回文串。设 s [ i , j ] s[i, j] s[i,j] 表示一段字符串(i 为左边界,j 为右边界),假设由中心扩展法已经找到一个回文串 s [ i , j ] s[i, j] s[i,j] ,再继续向两边扩展,如果 s [ i − 1 ] = s [ j + 1 ] s[i - 1] = s[j +1] s[i−1]=s[j+1], 则 s [ i − 1 , j + 1 ] s[i - 1, j +1] s[i−1,j+1] 也是一个回文串。这也就是原问题和子问题的联系。 - 定义状态:
根据上面的分析,我们定义状态为:dp[i][j] 表示字符串 s[i, j] 是不是回文串。所以我们创建布尔型的数组来存储状态,dp[i][j] = true 表示字符串 s[i, j] 是回文串;dp[i][j] = false 表示字符串 s[i, j] 不是回文串。 - 推导状态转移方程:
由问题拆解我们知道:若 s [ i + 1 , j − 1 ] s[i + 1, j -1] s[i+1,j−1] 是一个回文串,且 s [ i ] = s [ j ] s[i] = s[j] s[i]=s[j],则 s [ i , j ] s[i, j] s[i,j] 也是一个回文串,用公式表示为
d p [ i ] [ j ] = ( s [ i ] = = s [ j ] ) & & d p [ i + 1 ] [ j − 1 ] dp[i][j] = (s[i] == s[j]) \quad \&\& \quad dp[i +1][j - 1] dp[i][j]=(s[i]==s[j])&&dp[i+1][j−1] - 寻找边界条件:
(1)因为单个字符肯定是回文串,所以有 d p [ i ] [ i ] = t r u e dp[i][i] = true dp[i][i]=true。
(2)对于 d p [ i ] [ j ] = ( s [ i ] = = s [ j ] ) & & d p [ i + 1 ] [ j − 1 ] dp[i][j] = (s[i] == s[j]) \quad \&\& \quad dp[i +1][j - 1] dp[i][j]=(s[i]==s[j])&&dp[i+1][j−1] 如果字符串的长度小于等于 3,我们只需要判断 s[i] == s[j] 就能知道 s [ i , j ] s[i ,j] s[i,j] 是不是回文串,如 “aba”,“aa”。所以,当 s [ i ] = s [ j ] s[i] = s[j] s[i]=s[j] 且 j − i < 3 j - i < 3 j−i<3 时, d p [ i ] [ j ] = t r u e dp[i][j] = true dp[i][j]=true;而当 s [ i ] = s [ j ] s[i] = s[j] s[i]=s[j] 且 j − i ≥ 3 j - i \geq 3 j−i≥3 时,执行状态转移方程。这样也可以省掉很多计算。
参考代码:
class Solution {
public String longestPalindrome(String s) {
int length = s.length();
if(length < 2)
return s;
boolean[][] dp = new boolean[length][length];
// 初始化:这就相当于已经处理了长度为 1 的回文串,后面只需要处理长度大于 1 的回文串
for(int i = 0; i < length; i++){
dp[i][i] = true;
}
int maxLength = 1;// 记录最长回文串的长度
int left = 0;
for(int j = 1; j < length; j++){
for(int i = 0; i < j; i++){
if(s.charAt(i) == s.charAt(j)){
if(j - i < 3)
dp[i][j] = true;
else
dp[i][j] = dp[i + 1][j - 1];// 状态转移方程
}else{
dp[i][j] = false;
}
// 若dp[i][j] = true,说明s[i, j]是回文串,记录回文串的长度和左边界。
if(dp[i][j]){
int curLength = j - i + 1;
if(curLength > maxLength){
maxLength = curLength;// 更新回文串最大长度
left = i;// 左边界
}
}
}
}
return s.substring(left, left + maxLength);
}
}