线性动态规划:最长回文子序列(基础版——求解长度)
字数 1960 2025-12-22 05:41:20

好的,在回顾了你已学习过的海量题目后,我将为你讲解一个未被提及的线性动态规划经典问题。

线性动态规划:最长回文子序列(基础版——求解长度)

题目描述:

给你一个字符串 s,请你找出并返回这个字符串中最长的 回文子序列 的长度。

子序列 定义为:在不改变字符相对顺序的情况下,通过删除某些字符(也可以不删除)后组成的新字符串。
回文 定义为:一个字符串正着读和反着读是一样的。

示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列是 "bbbb"

示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列是 "bb"

解题过程循序渐进讲解:

我们的目标是,在一个序列(字符串)中,找出一个符合特定条件(回文)的最长子序列。由于“子序列”不要求连续,这提示我们不能简单地使用处理子数组(连续)的思路。动态规划是解决这类问题的有力工具。

步骤一:定义状态
我们定义一个二维数组 dp,其中 dp[i][j] 表示字符串 s 在区间 [i, j](即从下标 i 到下标 j 包含两端)内的子串中,最长回文子序列的长度。

  • ij 分别代表子串的起始和结束下标。
  • 我们的最终答案就是 dp[0][n-1],其中 n 是字符串 s 的长度。

步骤二:寻找状态转移方程
我们需要思考,如何从更小的子问题推导出 dp[i][j]。这取决于子串两端字符 s[i]s[j] 的关系。

  1. 情况一:s[i] == s[j]
    如果两端的字符相等,那么它们可以作为我们正在寻找的回文子序列的头和尾。在这种情况下,最优的回文子序列长度,就等于去掉头尾后,中间部分(子串 s[i+1...j-1])的最长回文子序列长度,再加上这两个相等的字符(长度+2)。

    • 转移方程:dp[i][j] = dp[i+1][j-1] + 2
    • 注意:当 i == j 时(即子串只有一个字符),i+1 会大于 j-1,我们定义这种情况的基础值是 0。
  2. 情况二:s[i] != s[j]
    如果两端的字符不相等,那么它们不可能同时作为最终回文子序列的头和尾。因此,我们必须从两个方向“舍弃”一个字符,看哪个能得到更长的回文子序列。

    • 选项A:舍弃 s[i],考虑子串 s[i+1...j] 的最长回文子序列长度,即 dp[i+1][j]
    • 选项B:舍弃 s[j],考虑子串 s[i...j-1] 的最长回文子序列长度,即 dp[i][j-1]
    • 我们选择长度更长的那个。
    • 转移方程:dp[i][j] = max(dp[i+1][j], dp[i][j-1])

步骤三:确定初始条件和边界情况

  • 单个字符:任何单个字符本身就是一个长度为 1 的回文串。所以对于所有 i,有 dp[i][i] = 1
  • 空串:当 i > j 时,表示我们考虑的子串是空的,其最长回文子序列长度自然为 0。这在我们的计算中,特别是处理 s[i] == s[j]i+1 > j-1 的情况(即 ij 相邻,j = i+1)时,需要作为基础。

步骤四:确定计算顺序
观察状态转移方程 dp[i][j] 依赖于 dp[i+1][j-1]dp[i+1][j]dp[i][j-1]。它依赖于更短的子串(i 更大或 j 更小)的结果。
因此,我们不能简单地按 i 从 0 到 n-1,j 从 0 到 n-1 的顺序计算。一个合理的顺序是:

  • 按子串的长度 L 从短到长进行计算。因为长度更长的子串结果依赖于长度更短的子串结果。
  • 外层循环 L2 遍历到 n(长度为1的已经在初始化中完成)。
  • 内层循环 i0 遍历到 n - L,那么 j 就固定为 i + L - 1

步骤五:示例推导(以 s = “bbbab” 为例)

初始化一个 5x5 的 dp 表,主对角线 dp[i][i] = 1

初始状态:
   b b b a b (j)
 b 1 0 0 0 0
 b 0 1 0 0 0
 b 0 0 1 0 0
 a 0 0 0 1 0
 b 0 0 0 0 1
(i)

长度 L=2:
i=0, j=1: s[0]=b, s[1]=b,相等 -> dp[0][1] = dp[1][0] + 2。因为 i>j,dp[1][0]=0,所以 dp[0][1]=2。
i=1, j=2: s[1]=b, s[2]=b,相等 -> dp[1][2] = dp[2][1] + 2 = 0+2 = 2。
i=2, j=3: s[2]=b, s[3]=a,不等 -> dp[2][3] = max(dp[3][3], dp[2][2]) = max(1,1)=1。
i=3, j=4: s[3]=a, s[4]=b,不等 -> dp[3][4] = max(dp[4][4], dp[3][3]) = max(1,1)=1。

更新后:
   b b b a b (j)
 b 1 2 0 0 0
 b 0 1 2 0 0
 b 0 0 1 1 0
 a 0 0 0 1 1
 b 0 0 0 0 1
(i)

长度 L=3:
i=0, j=2: s[0]=b, s[2]=b,相等 -> dp[0][2] = dp[1][1] + 2 = 1+2=3。
i=1, j=3: s[1]=b, s[3]=a,不等 -> dp[1][3] = max(dp[2][3], dp[1][2]) = max(1,2)=2。
i=2, j=4: s[2]=b, s[4]=b,相等 -> dp[2][4] = dp[3][3] + 2 = 1+2=3。

更新后:
   b b b a b (j)
 b 1 2 3 0 0
 b 0 1 2 2 0
 b 0 0 1 1 3
 a 0 0 0 1 1
 b 0 0 0 0 1
(i)

长度 L=4:
i=0, j=3: s[0]=b, s[3]=a,不等 -> dp[0][3] = max(dp[1][3], dp[0][2]) = max(2,3)=3。
i=1, j=4: s[1]=b, s[4]=b,相等 -> dp[1][4] = dp[2][3] + 2 = 1+2=3。

更新后:
   b b b a b (j)
 b 1 2 3 3 0
 b 0 1 2 2 3
 b 0 0 1 1 3
 a 0 0 0 1 1
 b 0 0 0 0 1
(i)

长度 L=5:
i=0, j=4: s[0]=b, s[4]=b,相等 -> dp[0][4] = dp[1][3] + 2 = 2+2=4。

最终结果:dp[0][4] = 4。

步骤六:代码实现要点(Python)

def longestPalindromeSubseq(s: str) -> int:
    n = len(s)
    # 初始化一个 n x n 的二维数组,所有值为0
    dp = [[0] * n for _ in range(n)]

    # 初始化:单个字符是长度为1的回文
    for i in range(n):
        dp[i][i] = 1

    # 按长度从2到n进行递推
    for length in range(2, n + 1): # length表示子串长度
        for i in range(n - length + 1):
            j = i + length - 1
            if s[i] == s[j]:
                # 如果子串长度为2,即i+1 > j-1的情况,中间部分长度为0
                if length == 2:
                    dp[i][j] = 2
                else:
                    dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])

    return dp[0][n-1]

总结:
这个问题的核心在于巧妙地定义了 dp[i][j] 来表示子串 s[i...j] 的最优解。通过分析首尾字符的关系,我们得到了简洁而强大的状态转移方程。计算顺序(按子串长度递增)确保了在计算 dp[i][j] 时,它所依赖的子问题都已经被计算出来。这是解决“序列类”区间动态规划问题的经典范式。理解了这个问题,就为后续更复杂的回文子序列变种(如统计个数、构造序列等)打下了坚实的基础。

好的,在回顾了你已学习过的海量题目后,我将为你讲解一个未被提及的线性动态规划经典问题。 线性动态规划:最长回文子序列(基础版——求解长度) 题目描述: 给你一个字符串 s ,请你找出并返回这个字符串中最长的 回文子序列 的长度。 子序列 定义为:在不改变字符相对顺序的情况下,通过删除某些字符(也可以不删除)后组成的新字符串。 回文 定义为:一个字符串正着读和反着读是一样的。 示例 1: 输入: s = "bbbab" 输出: 4 解释:一个可能的最长回文子序列是 "bbbb" 。 示例 2: 输入: s = "cbbd" 输出: 2 解释:一个可能的最长回文子序列是 "bb" 。 解题过程循序渐进讲解: 我们的目标是,在一个序列(字符串)中,找出一个符合特定条件(回文)的最长子序列。由于“子序列”不要求连续,这提示我们不能简单地使用处理子数组(连续)的思路。动态规划是解决这类问题的有力工具。 步骤一:定义状态 我们定义一个二维数组 dp ,其中 dp[i][j] 表示字符串 s 在区间 [i, j] (即从下标 i 到下标 j 包含两端)内的子串中,最长回文子序列的长度。 i 和 j 分别代表子串的起始和结束下标。 我们的最终答案就是 dp[0][n-1] ,其中 n 是字符串 s 的长度。 步骤二:寻找状态转移方程 我们需要思考,如何从更小的子问题推导出 dp[i][j] 。这取决于子串两端字符 s[i] 和 s[j] 的关系。 情况一: s[i] == s[j] 如果两端的字符相等,那么它们 可以 作为我们正在寻找的回文子序列的头和尾。在这种情况下,最优的回文子序列长度,就等于去掉头尾后,中间部分(子串 s[i+1...j-1] )的最长回文子序列长度,再加上这两个相等的字符(长度+2)。 转移方程: dp[i][j] = dp[i+1][j-1] + 2 注意 :当 i == j 时(即子串只有一个字符), i+1 会大于 j-1 ,我们定义这种情况的基础值是 0。 情况二: s[i] != s[j] 如果两端的字符不相等,那么它们 不可能同时 作为最终回文子序列的头和尾。因此,我们必须从两个方向“舍弃”一个字符,看哪个能得到更长的回文子序列。 选项A:舍弃 s[i] ,考虑子串 s[i+1...j] 的最长回文子序列长度,即 dp[i+1][j] 。 选项B:舍弃 s[j] ,考虑子串 s[i...j-1] 的最长回文子序列长度,即 dp[i][j-1] 。 我们选择长度更长的那个。 转移方程: dp[i][j] = max(dp[i+1][j], dp[i][j-1]) 步骤三:确定初始条件和边界情况 单个字符 :任何单个字符本身就是一个长度为 1 的回文串。所以对于所有 i ,有 dp[i][i] = 1 。 空串 :当 i > j 时,表示我们考虑的子串是空的,其最长回文子序列长度自然为 0。这在我们的计算中,特别是处理 s[i] == s[j] 且 i+1 > j-1 的情况(即 i 和 j 相邻, j = i+1 )时,需要作为基础。 步骤四:确定计算顺序 观察状态转移方程 dp[i][j] 依赖于 dp[i+1][j-1] 、 dp[i+1][j] 和 dp[i][j-1] 。它依赖于更短的子串( i 更大或 j 更小)的结果。 因此,我们不能简单地按 i 从 0 到 n-1, j 从 0 到 n-1 的顺序计算。一个合理的顺序是: 按子串的 长度 L 从短到长进行计算。因为长度更长的子串结果依赖于长度更短的子串结果。 外层循环 L 从 2 遍历到 n (长度为1的已经在初始化中完成)。 内层循环 i 从 0 遍历到 n - L ,那么 j 就固定为 i + L - 1 。 步骤五:示例推导(以 s = “bbbab” 为例) 初始化一个 5x5 的 dp 表,主对角线 dp[i][i] = 1 。 步骤六:代码实现要点(Python) 总结: 这个问题的核心在于巧妙地定义了 dp[i][j] 来表示子串 s[i...j] 的最优解。通过分析首尾字符的关系,我们得到了简洁而强大的状态转移方程。计算顺序(按子串长度递增)确保了在计算 dp[i][j] 时,它所依赖的子问题都已经被计算出来。这是解决“序列类”区间动态规划问题的经典范式。理解了这个问题,就为后续更复杂的回文子序列变种(如统计个数、构造序列等)打下了坚实的基础。