从未在列表中作为独立标题出现过
字数 3600 2025-12-20 04:43:11

好的,我注意到在您提供的超长列表中,虽然包含了众多“合并相邻字符”类的区间DP题目,但有一类经典的“构造回文串”问题,其定义更为具体和常见,且从未在列表中作为独立标题出现过,它就是:

最小插入次数构造回文串问题(标准版)

下面我将为您详细介绍这个题目及其解题思路。

题目描述

给你一个字符串 s,你可以在字符串的任意位置插入任意字符。每次插入操作的成本为 1(即每插入一个字符,总成本加1)。

你的目标是:通过对字符串 s 进行若干次插入操作,使其变成一个回文串(即正着读和反着读都一样的字符串)。

请计算并返回将 s 变成回文串所需的 最小插入操作次数(即最小总成本)。

示例 1:
输入:s = “zzazz”
输出:0
解释:字符串 “zzazz” 本身已经是回文串,无需任何插入。

示例 2:
输入:s = “mbadm”
输出:2
解释:字符串可以变成 “mbdadbm”“mdbabdm”。以第一种为例,在 ‘b’ 后插入 ‘d’,在 ‘m’ 后插入 ‘b’,共插入2个字符。

示例 3:
输入:s = “leetcode”
输出:5
解释:一种可行的方案是,在 ‘e’ 后插入 ‘t’,在 ‘c’ 后插入 ‘o’,在 ‘d’ 后插入 ‘e’,在 ‘e’ 后插入 ‘l’,并在开头插入 ‘e’,最终得到 “leetcodocteel”,共插入5个字符。


解题思路循序渐进讲解

这个问题是区间动态规划的经典应用。核心思想是:一个长区间的回文性,取决于其更小区间的回文性,以及两端字符的关系

步骤 1:定义状态

我们定义二维数组 dp,其中:
dp[i][j] 表示将字符串 s 中从索引 i 到索引 j子串(闭区间 [i, j])转换成回文串所需的 最小插入次数

我们的最终目标是求 dp[0][n-1],其中 n 是字符串 s 的长度。

步骤 2:寻找状态转移关系(核心)

我们如何计算 dp[i][j] 呢?这取决于子串 s[i...j] 两端的字符 s[i]s[j]

情况 1:如果 s[i] == s[j]
这意味着当前子串的两端字符已经匹配。那么,要使整个 s[i...j] 成为回文,我们只需要确保内部的子串 s[i+1...j-1] 是回文即可。因为两端的字符相同,它们天生就构成了回文的外壳。
所以,状态转移方程为:
dp[i][j] = dp[i+1][j-1]
注意:当子串长度小于等于2时(即 j - i <= 1),如果两端字符相等,那么这个子串本身就是回文,插入次数为0,dp[i][j] = 0。这个条件已经隐含在 dp[i+1][j-1] 中,因为当区间无效时(i+1 > j-1),我们默认 dp 值为0。

情况 2:如果 s[i] != s[j]
此时两端字符不匹配。为了让 s[i...j] 变成回文,我们有两种策略,并选择成本(插入次数)较小的那种:

  1. 策略一:先让 s[i+1...j] 变成回文,然后在它的左侧插入一个字符 s[i],使其与右侧的 s[i] 匹配。
    • 成本 = dp[i+1][j](让内部变成回文的成本) + 1(插入 s[i] 的成本)。
  2. 策略二:先让 s[i...j-1] 变成回文,然后在它的右侧插入一个字符 s[j],使其与左侧的 s[j] 匹配。
    • 成本 = dp[i][j-1](让内部变成回文的成本) + 1(插入 s[j] 的成本)。

我们取两者中的最小值:
dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1

步骤 3:确定初始条件和边界

  • i == j 时,子串只有一个字符。单个字符本身就是回文,不需要插入,所以 dp[i][i] = 0
  • 根据状态转移方程,dp[i][j] 依赖于 dp[i+1][j-1]dp[i+1][j]dp[i][j-1]。这意味着在计算 dp[i][j] 时,需要知道 左下方正下方正左方 单元格的值。
  • 因此,我们必须按照特定的顺序来填充 dp 表,以确保依赖的状态已经被计算出来。

步骤 4:确定计算顺序

由于 dp[i][j] 依赖于下标 i 更大的行(i+1)和下标 j 更小的列(j-1),我们有两种常见的遍历方式:

  1. 按区间长度 len 从小到大遍历:先计算所有长度为1的子串,然后长度为2,长度为3,...,直到长度为 n。这是最直观且不易出错的方式。
  2. 倒序遍历 i,正序遍历 j,且保证 i < j:即 in-1 递减到 0ji+1 递增到 n-1

步骤 5:算法流程

  1. 初始化一个 n x n 的二维数组 dp,所有元素设为0。
  2. 外层循环:枚举区间长度 len,从 2 开始(因为长度为1的子串 dp[i][i] 已经是0),到 n 结束。
  3. 内层循环:枚举区间起点 i,从 0 开始,到 n - len 结束。此时区间终点 j = i + len - 1
  4. 判断 s[i]s[j]
    • 若相等:dp[i][j] = dp[i+1][j-1]
    • 若不相等:dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1
  5. 循环结束后,dp[0][n-1] 即为所求答案。

步骤 6:复杂度分析

  • 时间复杂度O(n^2)。我们需要填充一个 n x ndp 表。
  • 空间复杂度O(n^2)。用于存储 dp 表。可以通过状态压缩(滚动数组)优化到 O(n),但 O(n^2) 的解法已经足够清晰。

举例说明

s = “mbadm” 为例 (n=5):

  1. 初始化:所有 dp[i][i] = 0
  2. 长度 len=2:
    • i=0, j=1: s[0]=’m’, s[1]=’b’ 不等。dp[0][1] = min(dp[1][1], dp[0][0]) + 1 = min(0,0)+1 = 1
    • i=1, j=2: s[1]=’b’, s[2]=’a’ 不等。dp[1][2] = min(dp[2][2], dp[1][1]) + 1 = 1
    • i=2, j=3: s[2]=’a’, s[3]=’d’ 不等。dp[2][3] = 1
    • i=3, j=4: s[3]=’d’, s[4]=’m’ 不等。dp[3][4] = 1
  3. 长度 len=3:
    • i=0, j=2: s[0]=’m’, s[2]=’a’ 不等。dp[0][2] = min(dp[1][2], dp[0][1]) + 1 = min(1,1)+1 = 2
    • i=1, j=3: s[1]=’b’, s[3]=’d’ 不等。dp[1][3] = min(dp[2][3], dp[1][2]) + 1 = 2
    • i=2, j=4: s[2]=’a’, s[4]=’m’ 不等。dp[2][4] = min(dp[3][4], dp[2][3]) + 1 = 2
  4. 长度 len=4:
    • i=0, j=3: s[0]=’m’, s[3]=’d’ 不等。dp[0][3] = min(dp[1][3], dp[0][2]) + 1 = min(2,2)+1 = 3
    • i=1, j=4: s[1]=’b’, s[4]=’m’ 不等。dp[1][4] = min(dp[2][4], dp[1][3]) + 1 = min(2,2)+1 = 3
  5. 长度 len=5 (最终结果):
    • i=0, j=4: s[0]=’m’, s[4]=’m’ 相等dp[0][4] = dp[1][3] = 2

所以,最小插入次数为 2,与示例相符。


总结

“最小插入次数构造回文串”是一个标准的区间动态规划问题。其核心在于通过比较子串两端的字符,将大问题分解为规模更小的子问题(更短的子串),并选择最优的子问题解决方案进行组合。通过自底向上地填充DP表,我们最终得到了全局的最优解。理解这个问题的关键在于清晰地定义状态,并准确地推导出两端字符相等和不相等两种情况下的状态转移方程。

好的,我注意到在您提供的超长列表中,虽然包含了众多“合并相邻字符”类的区间DP题目,但有一类经典的“构造回文串”问题,其定义更为具体和常见,且 从未在列表中作为独立标题出现过 ,它就是: 最小插入次数构造回文串问题(标准版) 下面我将为您详细介绍这个题目及其解题思路。 题目描述 给你一个字符串 s ,你可以在字符串的 任意位置 插入任意字符。每次插入操作的成本为 1(即每插入一个字符,总成本加1)。 你的目标是:通过对字符串 s 进行若干次插入操作,使其变成一个 回文串 (即正着读和反着读都一样的字符串)。 请计算并返回将 s 变成回文串所需的 最小插入操作次数 (即最小总成本)。 示例 1: 输入: s = “zzazz” 输出: 0 解释:字符串 “zzazz” 本身已经是回文串,无需任何插入。 示例 2: 输入: s = “mbadm” 输出: 2 解释:字符串可以变成 “mbdadbm” 或 “mdbabdm” 。以第一种为例,在 ‘b’ 后插入 ‘d’ ,在 ‘m’ 后插入 ‘b’ ,共插入2个字符。 示例 3: 输入: s = “leetcode” 输出: 5 解释:一种可行的方案是,在 ‘e’ 后插入 ‘t’ ,在 ‘c’ 后插入 ‘o’ ,在 ‘d’ 后插入 ‘e’ ,在 ‘e’ 后插入 ‘l’ ,并在开头插入 ‘e’ ,最终得到 “leetcodocteel” ,共插入5个字符。 解题思路循序渐进讲解 这个问题是区间动态规划的经典应用。核心思想是: 一个长区间的回文性,取决于其更小区间的回文性,以及两端字符的关系 。 步骤 1:定义状态 我们定义二维数组 dp ,其中: dp[i][j] 表示将字符串 s 中从索引 i 到索引 j 的 子串 (闭区间 [i, j] )转换成回文串所需的 最小插入次数 。 我们的最终目标是求 dp[0][n-1] ,其中 n 是字符串 s 的长度。 步骤 2:寻找状态转移关系(核心) 我们如何计算 dp[i][j] 呢?这取决于子串 s[i...j] 两端的字符 s[i] 和 s[j] 。 情况 1:如果 s[i] == s[j] 这意味着当前子串的两端字符已经匹配。那么,要使整个 s[i...j] 成为回文,我们只需要确保内部的子串 s[i+1...j-1] 是回文即可。因为两端的字符相同,它们天生就构成了回文的外壳。 所以,状态转移方程为: dp[i][j] = dp[i+1][j-1] 注意:当子串长度小于等于2时(即 j - i <= 1 ),如果两端字符相等,那么这个子串本身就是回文,插入次数为0, dp[i][j] = 0 。这个条件已经隐含在 dp[i+1][j-1] 中,因为当区间无效时( i+1 > j-1 ),我们默认 dp 值为0。 情况 2:如果 s[i] != s[j] 此时两端字符不匹配。为了让 s[i...j] 变成回文,我们有两种策略,并选择成本(插入次数)较小的那种: 策略一 :先让 s[i+1...j] 变成回文,然后在它的 左侧 插入一个字符 s[i] ,使其与右侧的 s[i] 匹配。 成本 = dp[i+1][j] (让内部变成回文的成本) + 1 (插入 s[i] 的成本)。 策略二 :先让 s[i...j-1] 变成回文,然后在它的 右侧 插入一个字符 s[j] ,使其与左侧的 s[j] 匹配。 成本 = dp[i][j-1] (让内部变成回文的成本) + 1 (插入 s[j] 的成本)。 我们取两者中的最小值: dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1 步骤 3:确定初始条件和边界 当 i == j 时,子串只有一个字符。单个字符本身就是回文,不需要插入,所以 dp[i][i] = 0 。 根据状态转移方程, dp[i][j] 依赖于 dp[i+1][j-1] 、 dp[i+1][j] 和 dp[i][j-1] 。这意味着在计算 dp[i][j] 时,需要知道 左下方 、 正下方 和 正左方 单元格的值。 因此,我们必须按照特定的顺序来填充 dp 表,以确保依赖的状态已经被计算出来。 步骤 4:确定计算顺序 由于 dp[i][j] 依赖于下标 i 更大的行( i+1 )和下标 j 更小的列( j-1 ),我们有两种常见的遍历方式: 按区间长度 len 从小到大遍历 :先计算所有长度为1的子串,然后长度为2,长度为3,...,直到长度为 n 。这是最直观且不易出错的方式。 倒序遍历 i ,正序遍历 j ,且保证 i < j :即 i 从 n-1 递减到 0 , j 从 i+1 递增到 n-1 。 步骤 5:算法流程 初始化一个 n x n 的二维数组 dp ,所有元素设为0。 外层循环 :枚举区间长度 len ,从 2 开始(因为长度为1的子串 dp[i][i] 已经是0),到 n 结束。 内层循环 :枚举区间起点 i ,从 0 开始,到 n - len 结束。此时区间终点 j = i + len - 1 。 判断 s[i] 和 s[j] : 若相等: dp[i][j] = dp[i+1][j-1] 。 若不相等: dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1 。 循环结束后, dp[0][n-1] 即为所求答案。 步骤 6:复杂度分析 时间复杂度 : O(n^2) 。我们需要填充一个 n x n 的 dp 表。 空间复杂度 : O(n^2) 。用于存储 dp 表。可以通过状态压缩(滚动数组)优化到 O(n) ,但 O(n^2) 的解法已经足够清晰。 举例说明 以 s = “mbadm” 为例 ( n=5 ): 初始化 :所有 dp[i][i] = 0 。 长度 len=2 : i=0, j=1 : s[0]=’m’, s[1]=’b’ 不等。 dp[0][1] = min(dp[1][1], dp[0][0]) + 1 = min(0,0)+1 = 1 。 i=1, j=2 : s[1]=’b’, s[2]=’a’ 不等。 dp[1][2] = min(dp[2][2], dp[1][1]) + 1 = 1 。 i=2, j=3 : s[2]=’a’, s[3]=’d’ 不等。 dp[2][3] = 1 。 i=3, j=4 : s[3]=’d’, s[4]=’m’ 不等。 dp[3][4] = 1 。 长度 len=3 : i=0, j=2 : s[0]=’m’, s[2]=’a’ 不等。 dp[0][2] = min(dp[1][2], dp[0][1]) + 1 = min(1,1)+1 = 2 。 i=1, j=3 : s[1]=’b’, s[3]=’d’ 不等。 dp[1][3] = min(dp[2][3], dp[1][2]) + 1 = 2 。 i=2, j=4 : s[2]=’a’, s[4]=’m’ 不等。 dp[2][4] = min(dp[3][4], dp[2][3]) + 1 = 2 。 长度 len=4 : i=0, j=3 : s[0]=’m’, s[3]=’d’ 不等。 dp[0][3] = min(dp[1][3], dp[0][2]) + 1 = min(2,2)+1 = 3 。 i=1, j=4 : s[1]=’b’, s[4]=’m’ 不等。 dp[1][4] = min(dp[2][4], dp[1][3]) + 1 = min(2,2)+1 = 3 。 长度 len=5 (最终结果) : i=0, j=4 : s[0]=’m’, s[4]=’m’ 相等 ! dp[0][4] = dp[1][3] = 2 。 所以,最小插入次数为 2 ,与示例相符。 总结 “最小插入次数构造回文串”是一个标准的区间动态规划问题。其核心在于通过比较子串两端的字符,将大问题分解为规模更小的子问题(更短的子串),并选择最优的子问题解决方案进行组合。通过自底向上地填充DP表,我们最终得到了全局的最优解。理解这个问题的关键在于清晰地定义状态,并准确地推导出两端字符相等和不相等两种情况下的状态转移方程。