好的,我注意到在您提供的超长列表中,虽然包含了众多“合并相邻字符”类的区间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表,我们最终得到了全局的最优解。理解这个问题的关键在于清晰地定义状态,并准确地推导出两端字符相等和不相等两种情况下的状态转移方程。