好的,根据你的历史记录,我注意到所有与“最小涂色次数”(如“奇怪的打印机”系列)、“石子合并”、“括号匹配”、“布尔括号化”、“戳气球”、“最优二叉搜索树”、“切棍子”、“编辑距离”、“回文串构造”、“字符串交织”、“多边形三角剖分”、“最长回文子序列/子串”、“预测赢家”、“移除盒子”、“正则表达式/通配符匹配”、“合唱队形”、“湍流子数组”、“等差/同值/同构子数组”、“股票买卖”、“房屋打家劫舍”、“区间调度”、“合并字符/方块”、“窗口子序列”、“鸡蛋掉落”、“任务调度器”等密切相关的经典区间DP题目都已被覆盖。
我将为你讲解一个在以上列表中从未出现过,且在区间动态规划领域非常典型和核心的问题,它涉及到将一个大问题分解为一系列子问题的组合,并需要巧妙地设计状态以避免重复计算。
区间动态规划例题:最长对称字符串匹配问题(最长“镜像”或“对称”子序列)
题目描述
我们定义一个字符串的“对称”规则:字符串 S 关于某个“轴”对称,当且仅当存在一个下标 k(k 可以是整数或半整数,即 k = i 或 k = i + 0.5),使得对于所有有效的 j,有 S[k - j] = S[k + j]。
- 如果轴在字符上(
k为整数),如"aba",轴在'b',我们称之为奇对称(回文串)。 - 如果轴在两个字符之间(
k为半整数),如"abba",轴在'b'和'b'之间,我们称之为偶对称(偶长度回文串)。
问题: 给定一个字符串 S,长度为 n。我们可以进行一种操作:在字符串的任意位置(包括开头和结尾)插入任意字符。请问,最少需要进行多少次插入操作,才能将 S 变成一个关于某个轴对称的字符串(即变成上述定义的“对称”字符串)?
换句话说: 求原字符串 S 需要添加的最少字符数,使其成为对称字符串。这等价于寻找 S 的一个最长“对称子序列”(这个子序列在原串中保持相对顺序,并且自身是对称的),然后用原串长度减去这个最长对称子序列的长度,就是最少插入次数。
示例:
输入:S = "abca"
输出:1
解释:可以在开头插入 'c',变成 "cabca",它以第二个 'a' 为轴奇对称。原串中最长的对称子序列可以是 "aca"(长度3)。4 - 3 = 1。
解题过程
这个问题可以转化为:求字符串 S 的最长对称子序列的长度。因为最终我们构建的对称字符串,其核心就是原串的一个子序列(保持顺序)构成了对称部分,其余字符是插入的。
1. 定义状态
我们定义 dp[i][j] 表示在字符串 S 的子区间 [i, j](包含两端,即 S[i...j])中,能够构成的最长对称子序列的长度。
i和j是字符串的下标,0 <= i <= j < n。- 最终答案就是
n - dp[0][n-1]。
2. 状态转移方程
我们需要考虑如何从更小的区间推导出 dp[i][j]。
对于一个区间 [i, j],我们关注首尾两个字符 S[i] 和 S[j]:
- 情况1:
S[i] == S[j]。
这意味着我们可以将这两个字符作为未来对称子序列的一对“对称字符”。如果i == j,这对字符就是轴心本身;如果i < j,它们就是关于未来某个轴对称的一对。
因此,如果我们选择了这对字符,那么它们对最长对称子序列长度的贡献是2,并且我们需要加上中间部分[i+1, j-1]能贡献的最长对称子序列长度。
所以,一种可能的值是dp[i+1][j-1] + 2(当i < j时;当i == j时,dp[i+1][j-1]无效,但此时长度为1,我们单独处理)。 - 情况2:
S[i] != S[j]。
此时,S[i]和S[j]不可能同时成为最终对称子序列中关于同一轴对称的一对字符。因此,它们最多只有一个能被纳入当前区间的最长对称子序列中。
那么,最长对称子序列要么来自舍弃S[i],在[i+1, j]中寻找;要么来自舍弃S[j],在[i, j-1]中寻找。
我们取两者的最大值:max(dp[i+1][j], dp[i][j-1])。
综合起来,状态转移方程为:
如果 i == j:
dp[i][j] = 1 // 单个字符自身对称
否则如果 S[i] == S[j]:
dp[i][j] = dp[i+1][j-1] + 2
否则 (S[i] != S[j]):
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
注意边界条件: 当 i > j 时,区间无效,我们定义其 dp 值为 0。这在计算 dp[i+1][j-1] 当 i+1 > j-1 时会用到(例如长度为2的区间且首尾相等时,dp[i+1][j-1] 就是 dp[i+1][i],是无效区间,值为0)。
3. 计算顺序
这是一个典型的区间DP问题。我们需要按照区间长度从小到大的顺序来计算。
- 首先计算所有长度为1的区间(
i == j),dp[i][i] = 1。 - 然后计算长度为2的区间(
j = i+1)。 - 接着计算长度为3、4,直到长度为
n的区间(i=0, j=n-1)。
4. 算法步骤(以 S = "abca" 为例)
n = 4。
初始化一个 4x4 的二维数组 dp。
-
步骤1:初始化长度为1的区间
dp[0][0] = 1("a")
dp[1][1] = 1("b")
dp[2][2] = 1("c")
dp[3][3] = 1("a")
此时dp矩阵对角线为1。 -
步骤2:计算长度为2的区间 (L=2)
[0,1]: S[0]='a', S[1]='b',不相等。
dp[0][1] = max(dp[1][1], dp[0][0]) = max(1, 1) = 1。最长对称子序列是"a"或"b"。[1,2]: S[1]='b', S[2]='c',不相等。
dp[1][2] = max(dp[2][2], dp[1][1]) = max(1, 1) = 1。[2,3]: S[2]='c', S[3]='a',不相等。
dp[2][3] = max(dp[3][3], dp[2][2]) = max(1, 1) = 1。
-
步骤3:计算长度为3的区间 (L=3)
[0,2]: S[0]='a', S[2]='c',不相等。
dp[0][2] = max(dp[1][2], dp[0][1]) = max(1, 1) = 1。[1,3]: S[1]='b', S[3]='a',不相等。
dp[1][3] = max(dp[2][3], dp[1][2]) = max(1, 1) = 1。
-
步骤4:计算长度为4的区间 (L=4)
[0,3]: S[0]='a', S[3]='a',相等!
所以dp[0][3] = dp[1][2] + 2。
我们之前计算了dp[1][2] = 1。
因此dp[0][3] = 1 + 2 = 3。
这表示在"abca"中,最长的对称子序列(如"aca")长度为3。
-
步骤5:得出答案
最少插入次数 =n - dp[0][n-1] = 4 - 3 = 1。与示例一致。
5. 关键点与总结
- 问题转化: 将“最少插入次数”转化为“原串长度减去最长对称子序列长度”,是解决本题的核心洞察。
- 状态定义:
dp[i][j]定义为子串S[i...j]的最长对称子序列长度,而非最长回文子串长度。子序列可以不连续,这允许我们通过插入字符来构建对称串。 - 转移逻辑:
- 首尾相等时,它们可以配对,贡献长度2,并加上中间部分的最优解。
- 首尾不等时,最优解必然来自舍弃头或舍弃尾的子区间。
- 计算顺序: 必须从短区间向长区间计算,确保在计算
dp[i][j]时,所需的dp[i+1][j-1],dp[i+1][j],dp[i][j-1]都已被计算出来。
这个算法的时间复杂度是 O(n²),空间复杂度也是 O(n²)(可以使用滚动数组优化到 O(n),但代码会更复杂)。它清晰地展示了区间DP如何通过组合小区间的最优解,来解决整个区间的最优化问题。