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);

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

相关推荐
xxwl5851 小时前
区域赛分类题解
c++·笔记·学习·算法
m0_674571971 小时前
力扣 hot 100 python
python·算法·leetcode
救救孩子把1 小时前
AI面试八股文-14_14-MCP-Skill-Function Calling
人工智能·安全·面试·职场和发展·推理·mcp·functioncall
青少儿编程课堂2 小时前
蓝桥杯+全国青少年信息素养大赛+电子学会PythonC/C++考级:最长公共子序列LCS解析
c++·python·算法·bfs·信息学竞赛
圣保罗的大教堂2 小时前
leetcode 3727. 最大交替平方和 中等
leetcode
倚楼听风语3 小时前
Kotlin 协程与 suspend 原理详解
java·算法·kotlin
Xin7703 小时前
LeetCode 200. 岛屿数量(DFS 深度优先遍历法)
leetcode
程序员清风3 小时前
双指针与滑动窗口:高效处理连续区间
开发语言·算法
代钦塔拉3 小时前
【C++/Qt 进阶】设计模式系列(2):工厂模式——从游戏角色到披萨店
c++·算法·设计模式