最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
思路一
var longestPalindrome = function(s) {
const n = s.length;
if (n < 2) return s; // 字符串长度小于2,直接返回原字符串
let start = 0, maxLength = 1; // 初始化最长回文子串的起始位置和最大长度
const dp = new Array(n); // 初始化dp数组,dp[i]表示以i为中心的最长回文子串的真正半径
// 遍历每一个字符作为回文中心
for (let i = 0; i < n; i++) {
// 奇数长度回文子串,以i为中心
dp[i] = expandAroundCenter(s, i, i);
// 偶数长度回文子串,以i和i+1为中心
if (i + 1 < n) {
const len2 = expandAroundCenter(s, i, i + 1);
dp[i + 1] = len2; // 注意这里更新dp[i+1],因为偶数长度的回文串的"中心"可以认为是i+1
}
// 更新最长回文子串的信息
if (dp[i] > maxLength) {
maxLength = dp[i];
start = i - Math.floor((dp[i] - 1) / 2);
}
if (i + 1 < n && dp[i + 1] > maxLength) {
maxLength = dp[i + 1];
start = i + 1 - Math.floor(dp[i + 1] / 2);
}
}
// 根据起始位置和最大长度截取最长回文子串
return s.substring(start, start + maxLength);
};
// 辅助函数,围绕中心扩展寻找回文串
function expandAroundCenter(s, left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
left--;
right++;
}
return right - left - 1; // 返回回文串的长度
}
讲解
动态规划在这里的关键是将问题分解为较小的子问题,并利用已解决的子问题结果来构建更大的解。对于寻找最长回文子串的问题,我们可以遵循以下步骤:
- 初始化:
○ 首先,初始化一个一维或二维的动态规划数组。对于一维DP方法,数组的长度与原字符串相同,每个元素表示以其索引为中心的最长回文子串的半径。
○ 初始化最长回文子串的起始位置和最大长度,通常起始位置设为0,长度设为1(单个字符本身就是最短的回文子串)。- 状态定义:
○ 设dp[i]表示以索引i为中心的最长回文子串的半径(对于奇数长度的回文)或实际长度减1(对于偶数长度的回文,想象一个虚拟中心位于两个字符之间)。- 状态转移方程:
○ 对于每个中心,尝试向两边扩展,直到遇到不匹配的字符为止。利用已计算的子串信息来避免重复计算。
○ 对于奇数长度的回文,中心是单一字符,检查s[i-(dp[i]-1)]和s[i+(dp[i]-1)]是否相等。
○ 对于偶数长度的回文,中心位于两个字符间,检查s[i-(dp[i]+1)/2]和s[i+(dp[i]+1)/2]是否相等。- 遍历和更新:
○ 遍历字符串中的每个字符,对每个字符应用上述状态转移方程,更新最长回文子串的信息(起始位置和最大长度)。- 结果提取:
○ 根据最终的最长回文子串的起始位置和最大长度,从原字符串中截取这个子串并返回。通过这种分而治之的策略,我们能够在每个字符上计算出以它为中心的最长回文子串信息,进而找到整个字符串中的最长回文子串。这种方法相比暴力解法大大提高了效率,尤其是通过空间优化使用一维数组时,既保持了问题解决的高效性,又降低了空间复杂度。