leetcode 5最长回文子串

一、完整代码(精简注释)

复制代码
class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.length();
        int maxlen = 1;  // 最长回文子串的长度
        int start = 0;   // 最长回文子串的起始下标

        if (n < 2) return s;

        // dp[i][j] 表示 s[i...j] 是否为回文子串
        vector<vector<bool>> dp(n, vector<bool>(n, false));

        // 先遍历右端点,再遍历左端点
        for (int j = 0; j < n; j++) {
            for (int i = 0; i <= j; i++) {

                // 左右字符相等,才可能是回文
                if (s[i] == s[j]) {

                    // 长度 <= 3 或中间子串是回文
                    if (j - i <= 2 || dp[i+1][j-1]) {
                        dp[i][j] = true;

                        int len = j - i + 1;

                        // 更新最长回文子串
                        if (len > maxlen) {
                            start = i;
                            maxlen = len;
                        }
                    }
                }
            }
        }

        // 截取最长回文子串
        return s.substr(start, maxlen);
    }
};

二、动态规划五步总结

1. 确定 dp 数组及下标含义

复制代码
vector<vector<bool>> dp(n, vector<bool>(n, false));

dp[i][j] 表示字符串 s 从下标 i 到 j 的子串是否为回文。

  • dp[i][j] = true:是回文子串。

  • dp[i][j] = false:不是回文子串。

例如:

复制代码
s = "ababa"

dp[0][4] = true    // ababa
dp[1][3] = true    // bab
dp[0][1] = false   // ab

2. 确定状态转移方程

判断 s[i...j] 是否为回文,需要考虑两种情况。

情况一:左右字符不相等

复制代码
s[i] != s[j]

说明当前子串一定不是回文,dp[i][j] 保持 false。

情况二:左右字符相等

复制代码
s[i] == s[j]

此时还需要判断中间子串是否为回文。

如果子串长度小于等于 3,只需要左右字符相等即可。

复制代码
"a"     长度为1
"aa"    长度为2
"aba"   长度为3

如果子串长度大于 3,则需要判断中间部分:

复制代码
dp[i+1][j-1]

因此状态转移为:

复制代码
if (s[i] == s[j]) {
    if (j - i <= 2 || dp[i+1][j-1]) {
        dp[i][j] = true;
    }
}

注意:

j - i <= 2 等价于子串长度小于等于 3,因为子串长度是 j - i + 1。

3. 初始化 dp 数组

复制代码
vector<vector<bool>> dp(n, vector<bool>(n, false));

首先将所有状态初始化为 false。

对于单个字符:

复制代码
i == j

由于字符与自身相等,并且满足 j-i <= 2,因此会自动将:

复制代码
dp[i][i] = true;

所以不需要额外初始化对角线。

另外:

复制代码
int maxlen = 1;
int start = 0;

表示初始最长回文子串为第一个字符,长度为 1。

4. 确定遍历顺序

复制代码
for (int j = 0; j < n; j++) {
    for (int i = 0; i <= j; i++) {
        // 状态转移
    }
}

采用先遍历右端点 j,再遍历左端点 i 的方式。

原因是:

计算 dp[i][j] 时,需要依赖:

复制代码
dp[i+1][j-1]

也就是说,当前子串是否为回文,依赖于中间子串是否为回文。

而中间子串的右端点是 j-1,比当前右端点 j 小。

因此先从小到大遍历 j,能够保证依赖的状态已经计算完成。

核心:先计算短子串,再利用短子串的结果判断长子串。

5. 举例推导 dp 数组

假设:

复制代码
s = "babad";

下标:

复制代码
下标: 0 1 2 3 4
字符: b a b a d

当 j = 2,i = 0 时:

复制代码
s[0...2] = "bab"

判断:

复制代码
s[0] == s[2]   // b == b,成立
j - i <= 2     // 2 <= 2,成立

因此:

复制代码
dp[0][2] = true;

计算长度:

复制代码
len = 2 - 0 + 1 = 3;

由于:

复制代码
len > maxlen

更新:

复制代码
start = 0;
maxlen = 3;

之后找到 "aba" 时,其长度同样为 3,因此不会更新。

最终返回:

复制代码
s.substr(0, 3);  // "bab"

三、复杂度分析

时间复杂度:O(n²)

两层循环枚举左右端点,每个 dp 状态只需要常数时间进行判断。

空间复杂度:O(n²)

创建了一个 n × n 的二维 bool 数组存储回文状态。

四、核心总结(适合 CSDN 笔记)

  1. 本题使用二维动态规划,利用较短子串的回文状态判断较长子串是否为回文。

  2. dpij 表示从下标 i 到 j 的子串是否为回文。

  3. 当 si != sj 时,当前子串一定不是回文。

  4. 当 si == sj 时,如果子串长度小于等于 3,则当前子串一定是回文;否则需要判断中间子串 dpi+1j-1 是否为 true。

  5. 初始化二维 dp 数组为 false,单个字符的回文状态会在遍历过程中自动更新。

  6. 遍历顺序为先枚举右端点 j,再枚举左端点 i,保证计算当前状态时,依赖的中间子串状态已经计算完成。

  7. 每找到一个回文子串,就计算其长度 len = j-i+1。如果超过 maxlen,则更新最长长度 maxlen 和起始下标 start。

  8. 最终使用 s.substr(start, maxlen) 截取最长回文子串。

最重要的三个知识点

① 回文判断

复制代码
if (s[i] == s[j] && (j-i <= 2 || dp[i+1][j-1])) {
    dp[i][j] = true;
}

② 最长长度更新

复制代码
int len = j - i + 1;
if (len > maxlen) {
    start = i;
    maxlen = len;
}

③ 截取字符串

复制代码
return s.substr(start, maxlen);

一句话总结:先判断左右字符是否相等,再判断中间子串是否为回文,同时记录出现过的最长回文子串。

相关推荐
Dream it possible!10 小时前
LeetCode 面试经典 150_多维动态规划_交错字符串(97_C++_中等)
算法·leetcode·面试·动态规划
心抵鹊10 小时前
算法之双指针系列(一)
算法·leetcode
十二10 小时前
【洛谷题解】P1083 [NOIP 2012 提高组] 借教室(二分答案 + 差分)
数据结构·c++·算法·蓝桥杯
小此方11 小时前
[算法实战论:动态规划]回文串问题的dp解法,回文串问题的母题,以及以此为基础的派生题
算法·动态规划
Pniubi12 小时前
力扣560.和为k的子数组
算法·leetcode·职场和发展
D e re k ZH12 小时前
C++ 从零入门(一):基础语法与程序结构
数据结构·c++·算法
Pniubi13 小时前
力扣438.找到字符串中所有字母异位词
算法·leetcode·职场和发展
東隅已逝,桑榆非晚15 小时前
算法6. 双指针法-查找总价格为目标的两个商品
c++·学习·算法
Eyfcom15 小时前
〖物联网终端实战 01〗UWB 室内高精度定位:仓储 AGV 与人员的厘米级轨迹追踪
物联网·算法·物流系统·快递系统·跨境物流快递系统·物联网终端实战
bro_Java66616 小时前
《排序的底层逻辑:比较、交换与分治》
java·算法·排序算法