题目:最长回文子串 (Longest Palindromic Substring) —— 动态规划解法
字数 2561 2025-12-11 10:36:10

好的,我已经记住了你已学过的题目列表。这次我为你讲解一个线性动态规划领域中非常经典,但可能在你列表中尚未出现过的题目。

题目:最长回文子串 (Longest Palindromic Substring) —— 动态规划解法

题目描述
给定一个字符串 s,请你找出 s 中最长的回文子串(连续的字符序列)。
回文串是指正着读和反着读都一样的字符串。

示例
输入:s = "babad"
输出:"bab" 或 "aba"(都有效)
输入:s = "cbbd"
输出:"bb"

约束条件

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成。

解题过程详解(动态规划思路)

我们要找的是最长回文子串,这是一个“子串”问题,意味着结果必须是原字符串中连续的一段。动态规划非常适合解决这种“最优子结构”问题。

第一步:定义状态

我们需要一个二维数组 dp[i][j] 来表示某个子串是否是回文。

  • dp[i][j] 表示:以索引 i 为起始,以索引 j 为结束的子串 s[i..j] 是否是回文串
  • 其值是一个布尔值:True 代表是回文,False 代表不是回文。
  • 我们的最终目标就是找出所有 dp[i][j] == True 的子串中,j - i + 1(长度)最大的那个。

第二步:寻找状态转移方程

如何由已知的小问题推导出大问题呢?核心思路是:一个长串是不是回文,取决于它的“核心”和两端的字符

  1. 基本情况(最小子问题)

    • 单个字符:对于任意 i,子串 s[i..i] 只有一个字符,当然是回文。所以 dp[i][i] = True
    • 两个字符:子串 s[i..i+1] 是回文,当且仅当这两个字符相等。所以 dp[i][i+1] = (s[i] == s[i+1])
  2. 一般情况(长度 >= 3 的子串)

    • 对于子串 s[i..j](其中 j - i >= 2),它要成为回文,必须满足两个条件:
      a. 它两端的字符必须相等:s[i] == s[j]
      b. 去掉两端字符后剩下的“核心”部分 s[i+1..j-1] 也必须是回文串,即 dp[i+1][j-1] == True
    • 因此,状态转移方程为:
      dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1]

第三步:确定填表顺序

观察状态转移方程 dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1]
要计算 dp[i][j],我们需要知道 dp[i+1][j-1] 的结果,也就是左下方格子的值。
这意味着我们不能按简单的 i0nj0n 的顺序来填表,因为 i+1 行可能还没算。

一个有效的填表顺序是:

  1. 按子串长度 L 从小到大进行枚举。
  2. 对于每个固定长度 L,枚举所有可能的起始位置 i,那么结束位置 j = i + L - 1
  3. 因为计算 dp[i][j] 时,用到的 dp[i+1][j-1] 对应的是长度 L-2 的子串,这个值在我们按长度递增的顺序计算时,肯定已经计算出来了。

这种顺序保证了依赖的子问题总是先被解决。

第四步:初始化与边界情况

  • 初始化一个 n x n 的二维数组(n 为字符串长度),所有值先设为 False
  • 长度为1的子串(对角线)初始化为 Truedp[i][i] = True
  • 长度为2的子串(j = i+1)根据 s[i] == s[i+1] 来初始化。这一步可以融入到主循环中处理,也可以单独处理。

第五步:记录答案

在填充 dp 表的过程中,我们随时追踪当前找到的最长回文子串的起始位置长度
一旦发现 dp[i][j] == True,就检查当前子串长度 L = j - i + 1 是否大于已知的最大长度。如果是,就更新记录。

第六步:算法流程(伪代码)

def longestPalindrome(s: str) -> str:
    n = len(s)
    if n < 2:
        return s

    # 步骤1:初始化dp表
    dp = [[False] * n for _ in range(n)]

    # 记录最长回文的起始位置和长度
    start = 0
    max_len = 1

    # 步骤2:初始化长度为1的子串
    for i in range(n):
        dp[i][i] = True

    # 步骤3:按子串长度 L 从小到大枚举 (L从2到n)
    for L in range(2, n + 1):
        # 枚举起始位置 i
        for i in range(n):
            # 计算结束位置 j
            j = i + L - 1
            # 如果 j 越界,直接跳出内层循环
            if j >= n:
                break

            # 检查两端字符
            if s[i] != s[j]:
                dp[i][j] = False
            else:
                # 核心转移方程
                if L == 2:  # 长度为2的特殊情况
                    dp[i][j] = True
                else:       # 长度大于2
                    dp[i][j] = dp[i + 1][j - 1]

            # 步骤4:更新答案
            if dp[i][j] and L > max_len:
                max_len = L
                start = i

    # 步骤5:根据记录的起始位置和最大长度返回子串
    return s[start: start + max_len]

第七步:复杂度分析

  • 时间复杂度:O(n²)。我们有两层循环,外层枚举长度 O(n),内层枚举起点 O(n),内部操作为 O(1)。
  • 空间复杂度:O(n²)。我们需要一个 n x n 的二维 DP 表来存储中间状态。

第八步:示例推演 (s = “babad”)

  1. 初始化 n=5, dp 表对角线为 Truemax_len=1, start=0
  2. 长度 L=2:
    • i=0, j=1: s[0]=’b’, s[1]=’a’ 不等 -> dp[0][1]=False
    • i=1, j=2: ‘a’ != ‘b’ -> False
    • i=2, j=3: ‘b’ == ‘a’? No -> False
    • i=3, j=4: ‘a’ == ‘d’? No -> False
      无更新。
  3. 长度 L=3:
    • i=0, j=2: s[0]=’b’, s[2]=’b’ 相等,且 L=3 >2,看核心 dp[1][1]=True -> dp[0][2]=True。长度3 > 1,更新 max_len=3, start=0
    • i=1, j=3: ‘a’ == ‘a’ 相等,核心 dp[2][2]=True -> dp[1][3]=True。长度3等于当前 max_len,不更新起始位置(或者可以更新,取决于题目要求,通常返回任意一个即可)。
    • i=2, j=4: ‘b’ != ‘d’ -> False。
  4. 长度 L=4, L=5:遍历后发现没有更长的回文子串。
  5. 最终返回 s[0:0+3] = “bab”。算法同样能找到 ”aba”,取决于内层循环顺序和更新策略。

总结:这个动态规划解法通过定义子问题 dp[i][j],利用回文串的“两边相等且内部也是回文”的性质,建立了状态转移方程。通过按子串长度递增的顺序填表,我们能够从最小的回文串(单个字符)开始,逐步构造出更长的回文串,并最终找到最长的那个。这个思路清晰,是理解和掌握区间DP(一种二维线性DP)的绝佳例题。

好的,我已经记住了你已学过的题目列表。这次我为你讲解一个线性动态规划领域中非常经典,但可能在你列表中尚未出现过的题目。 题目:最长回文子串 (Longest Palindromic Substring) —— 动态规划解法 题目描述 给定一个字符串 s ,请你找出 s 中最长的回文子串(连续的字符序列)。 回文串是指正着读和反着读都一样的字符串。 示例 输入:s = "babad" 输出:"bab" 或 "aba"(都有效) 输入:s = "cbbd" 输出:"bb" 约束条件 1 <= s.length <= 1000 s 仅由数字和英文字母组成。 解题过程详解(动态规划思路) 我们要找的是最长回文子串,这是一个“子串”问题,意味着结果必须是原字符串中 连续 的一段。动态规划非常适合解决这种“最优子结构”问题。 第一步:定义状态 我们需要一个二维数组 dp[i][j] 来表示某个子串是否是回文。 dp[i][j] 表示: 以索引 i 为起始,以索引 j 为结束的子串 s[i..j] 是否是回文串 。 其值是一个布尔值: True 代表是回文, False 代表不是回文。 我们的最终目标就是找出所有 dp[i][j] == True 的子串中, j - i + 1 (长度)最大的那个。 第二步:寻找状态转移方程 如何由已知的小问题推导出大问题呢?核心思路是: 一个长串是不是回文,取决于它的“核心”和两端的字符 。 基本情况(最小子问题) : 单个字符:对于任意 i ,子串 s[i..i] 只有一个字符,当然是回文。所以 dp[i][i] = True 。 两个字符:子串 s[i..i+1] 是回文,当且仅当这两个字符相等。所以 dp[i][i+1] = (s[i] == s[i+1]) 。 一般情况(长度 >= 3 的子串) : 对于子串 s[i..j] (其中 j - i >= 2 ),它要成为回文,必须满足两个条件: a. 它两端的字符必须相等: s[i] == s[j] b. 去掉两端字符后剩下的“核心”部分 s[i+1..j-1] 也必须是回文串,即 dp[i+1][j-1] == True 因此,状态转移方程为: dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1] 第三步:确定填表顺序 观察状态转移方程 dp[i][j] = (s[i] == s[j]) and dp[i+1][j-1] 。 要计算 dp[i][j] ,我们需要知道 dp[i+1][j-1] 的结果,也就是 左下方格子 的值。 这意味着我们不能按简单的 i 从 0 到 n , j 从 0 到 n 的顺序来填表,因为 i+1 行可能还没算。 一个有效的填表顺序是: 按子串长度 L 从小到大 进行枚举。 对于每个固定长度 L ,枚举所有可能的起始位置 i ,那么结束位置 j = i + L - 1 。 因为计算 dp[i][j] 时,用到的 dp[i+1][j-1] 对应的是长度 L-2 的子串,这个值在我们按长度递增的顺序计算时,肯定已经计算出来了。 这种顺序保证了依赖的子问题总是先被解决。 第四步:初始化与边界情况 初始化一个 n x n 的二维数组( n 为字符串长度),所有值先设为 False 。 长度为1的子串(对角线)初始化为 True : dp[i][i] = True 。 长度为2的子串( j = i+1 )根据 s[i] == s[i+1] 来初始化。这一步可以融入到主循环中处理,也可以单独处理。 第五步:记录答案 在填充 dp 表的过程中,我们随时追踪当前找到的最长回文子串的 起始位置 和 长度 。 一旦发现 dp[i][j] == True ,就检查当前子串长度 L = j - i + 1 是否大于已知的最大长度。如果是,就更新记录。 第六步:算法流程(伪代码) 第七步:复杂度分析 时间复杂度 :O(n²)。我们有两层循环,外层枚举长度 O(n),内层枚举起点 O(n),内部操作为 O(1)。 空间复杂度 :O(n²)。我们需要一个 n x n 的二维 DP 表来存储中间状态。 第八步:示例推演 (s = “babad”) 初始化 n=5 , dp 表对角线为 True 。 max_len=1 , start=0 。 长度 L=2: i=0, j=1: s[ 0]=’b’, s[ 1]=’a’ 不等 -> dp[0][1]=False i=1, j=2: ‘a’ != ‘b’ -> False i=2, j=3: ‘b’ == ‘a’? No -> False i=3, j=4: ‘a’ == ‘d’? No -> False 无更新。 长度 L=3: i=0, j=2: s[ 0]=’b’, s[ 2]=’b’ 相等,且 L=3 >2,看核心 dp[1][1]=True -> dp[0][2]=True 。长度3 > 1,更新 max_len=3 , start=0 。 i=1, j=3: ‘a’ == ‘a’ 相等,核心 dp[2][2]=True -> dp[1][3]=True 。长度3等于当前 max_len ,不更新起始位置(或者可以更新,取决于题目要求,通常返回任意一个即可)。 i=2, j=4: ‘b’ != ‘d’ -> False。 长度 L=4, L=5:遍历后发现没有更长的回文子串。 最终返回 s[0:0+3] = “bab” 。算法同样能找到 ”aba” ,取决于内层循环顺序和更新策略。 总结 :这个动态规划解法通过定义子问题 dp[i][j] ,利用回文串的“两边相等且内部也是回文”的性质,建立了状态转移方程。通过按子串长度递增的顺序填表,我们能够从最小的回文串(单个字符)开始,逐步构造出更长的回文串,并最终找到最长的那个。这个思路清晰,是理解和掌握区间DP(一种二维线性DP)的绝佳例题。