奇怪的打印机(最小打印次数问题,进阶版:每次打印必须覆盖一个连续区间,且每次只能打印一种颜色,已打印部分可被覆盖,求打印整个目标字符串的最少打印次数)
字数 5082 2025-12-14 22:55:03

奇怪的打印机(最小打印次数问题,进阶版:每次打印必须覆盖一个连续区间,且每次只能打印一种颜色,已打印部分可被覆盖,求打印整个目标字符串的最少打印次数)

题目描述:
给定一个字符串 s,表示最终要打印出的目标字符串。初始时,打印纸是空白的(可以视为由任意数量的相同字符组成,例如全是 'a',但最终我们需要打印出 s)。你有一台奇怪的打印机,它每次操作必须打印一个连续区间,并且这个区间内的所有字符必须被打印成同一种颜色(即同一个字符)。已打印的部分可以被后续打印覆盖。问至少需要多少次打印操作,才能得到目标字符串 s。

示例:
输入:s = "aaabbb"
输出:2
解释:先打印整个区间为 'a'(得到 "aaaaaa"),再打印后三个字符为 'b'(得到 "aaabbb")。

解题过程:

这个问题是区间动态规划的典型应用。我们需要逐步推导出最优的打印策略。核心思路是:将字符串分割成子区间,考虑如何在子区间内以最少的打印次数得到目标字符串。

第一步:明确状态定义

定义 dp[i][j] 为打印出子串 s[i...j](下标从 i 到 j,包含两端)所需的最少打印次数。
目标是求 dp[0][n-1],其中 n 是字符串长度。

第二步:分析基础情况

  • 当区间长度为 1 时(即 i == j),只需要打印一次即可得到该字符。
    所以 dp[i][i] = 1

第三步:状态转移分析(关键)

对于区间 [i, j],我们考虑第一次打印操作如何覆盖这个区间。由于每次打印必须是一个连续区间且全部打印为同一种颜色,我们可以考虑第一次打印覆盖了区间 [i, k],且这次打印的颜色设为 s[i](因为最终 s[i] 的颜色是固定的,第一次打印可以选择打印成 s[i],这样后续再覆盖其他部分)。注意,第一次打印不一定只打印一个字符,它可以打印一段连续的区间。

但更通用的思路是:

  1. 首先,如果 s[i] == s[j],那么我们可以利用一个性质:可以在打印区间 [i, j] 时,先将整个区间打印成 s[i](或 s[j]),然后在内部调整。但更精确的优化是:因为 s[i] 和 s[j] 相同,我们可以将打印 s[i] 和 s[j] 的操作合并考虑,即先打印整个区间为 s[i],然后再在内部覆盖。但动态规划中更常用的是:如果 s[i] == s[k](i < k <= j),那么可以先打印区间 [i, k] 为 s[i],然后问题分解为 [i+1, k-1] 和 [k, j]。但这样处理比较复杂。

更简洁且正确的状态转移方程是:
我们考虑将区间分成两段:[i, k][k+1, j]
如果两段完全独立打印,则总打印次数为 dp[i][k] + dp[k+1][j]
但这里有一个优化:如果 s[i] 和 s[k+1] 相同,我们是否可以在某次打印中同时覆盖它们,从而减少一次打印?
实际上,更精确的优化是:如果 s[i] == s[k+1],那么我们可以让第一次打印的颜色为 s[i],覆盖区间 [i, m],然后后续处理其他部分。但这样在状态转移中不易直接表示。

经典且正确的状态转移方法是:
对于区间 [i, j],我们考虑第一次打印操作打印了区间 [i, k],且颜色为 s[i]。那么这次打印之后,位置 i 已经正确了。接下来我们需要处理剩余部分:区间 [i+1, j]。但注意,由于第一次打印覆盖了 [i, k],所以区间 [i+1, k] 现在也是颜色 s[i]。如果 s[i] 和 s[k+1] 不同,那么 [i+1, k] 可能需要被覆盖。但更简单的思路是:将问题分解为:先打印出 s[i] 正确的部分,再处理剩下的。

但最常用的状态转移方程是(经过验证的):
dp[i][j] = dp[i][j-1] 如果 s[i] == s[j]
为什么?因为如果 s[i] == s[j],我们可以在打印 s[i] 的时候,一次性将整个区间 [i, j] 打印成 s[i],然后再在区间内部覆盖修改,而不需要额外增加一次打印来专门处理 s[j]。但更准确的理解是:我们可以将 s[j] 视为和 s[i] 一起打印,所以次数不会比打印 [i, j-1] 多。但这是否正确?考虑例子 "aba",如果 i=0, j=2, s[0]=a, s[2]=a,那么 dp[0][2] 应该等于 2(先打印整个区间为 'a',再打印中间为 'b'),而 dp[0][1] 也是 2(先打印 "aa",再打印第二个为 'b'),所以相等。但注意,这个等式并不总是成立,所以我们需要更精确的推导。

实际上,正确的状态转移方程是:

  • 首先,可以单独打印位置 i,然后处理 [i+1, j],即 dp[i][j] = 1 + dp[i+1][j]
  • 其次,我们尝试节省操作:如果在 [i+1, j] 中有一个位置 k,使得 s[i] == s[k],那么我们可以将区间 [i, k] 在一次打印中完成(打印成 s[i]),然后问题分解为:先打印出 [i+1, k-1] 的部分(因为 [i, k] 已经打印成 s[i],所以 [i+1, k-1] 需要被覆盖成正确的字符),再加上打印 [k, j] 的部分。但注意,由于 s[i] == s[k],打印 [i, k] 为 s[i] 后,位置 k 已经是正确的颜色了,所以问题变成:打印 [i+1, k-1] 和打印 [k, j] 两部分。但这两部分可能有重叠,所以更好的分解是:打印 [i+1, k-1] 和打印 [k+1, j],但这样不连续。

经过整理,正确的状态转移方程为:
dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k+1][j])s[i] == s[k] 且 i < k <= j。
但这是不完整的,因为当 s[i] == s[k] 时,我们可以将区间 [i, k] 用同一次打印完成,所以总的打印次数可能是:打印 [i, k] 为 s[i] 这一次,再加上打印 [i+1, k-1] 的次数(因为 [i, k] 已经打印成 s[i],所以只需要处理内部区间 [i+1, k-1] 即可),再加上打印 [k+1, j] 的次数。但注意,打印 [i+1, k-1] 实际上就是 dp[i+1][k-1],而打印 [k+1, j] 是 dp[k+1][j]。但这样会把区间割裂。

实际上,更标准且正确的状态转移方程是(来自 LeetCode 664 题的标准解法):
dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k][j])s[i] == s[k] 且 i < k <= j。
但这样需要处理边界。常用的正确形式是:
dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k][j] - 1) 如果 s[i] == s[k]。
但这里为了清晰,我们采用另一种更直观的推导。

第四步:标准状态转移方程

定义 dp[i][j] 为打印 s[i...j] 的最少打印次数。
基础情况:dp[i][i] = 1
对于 i < j

  1. 先考虑最差情况:每次只打印一个字符,则 dp[i][j] = j - i + 1
  2. 优化:如果 s[i] == s[j],那么我们可以将打印 s[i] 和 s[j] 合并为同一次操作(在打印区间 [i, j] 时先整体打印成 s[i],再处理内部),所以 dp[i][j] = min(dp[i][j], dp[i][j-1])。注意,这里 dp[i][j-1] 表示打印 s[i...j-1] 的次数,因为 s[j] 可以和 s[i] 一起打印,所以不会增加次数。
  3. 进一步优化:在区间 [i, j] 中,如果有一个位置 k (i < k <= j) 使得 s[i] == s[k],那么我们可以将区间 [i, k] 在一次操作中打印成 s[i],然后问题分解为:打印 [i+1, k-1] 和打印 [k, j]。但注意,打印 [k, j] 时,s[k] 已经正确了,所以实际上只需要打印 [k, j] 这部分。但更准确的分解是:打印 [i, k] 为 s[i] 这一次操作,加上打印 [i+1, k-1] 的次数,再加上打印 [k+1, j] 的次数。但这样比较复杂。

经过验证,标准且正确的状态转移方程为:

dp[i][j] = dp[i][j-1]  # 如果 s[i] == s[j]
否则:
dp[i][j] = min(dp[i][k] + dp[k+1][j]) 对于所有 i <= k < j

但这样在 s[i] == s[j] 时,我们可以进一步优化为 dp[i][j] = dp[i][j-1]
为什么?因为如果 s[i] == s[j],我们可以在打印 s[i] 的时候,一次性覆盖到 j,这样打印 s[j] 就不需要额外次数,所以次数等于打印 s[i...j-1] 的次数。

第五步:示例推导

以 s = "ababa" 为例,n=5。
初始化 dp 矩阵,对角线 dp[i][i] = 1。
计算区间长度从 2 到 4(0-indexed)。

  • 区间 [0,1]: s[0]='a', s[1]='b',不相等,所以 dp[0][1] = min(dp[0][0]+dp[1][1]) = 1+1=2。
  • 区间 [1,2]: 'b' 和 'a' 不同,dp[1][2]=2。
  • 区间 [2,3]: 'a' 和 'b' 不同,dp[2][3]=2。
  • 区间 [3,4]: 'b' 和 'a' 不同,dp[3][4]=2。
  • 区间 [0,2]: s[0]='a', s[2]='a' 相等,所以 dp[0][2] = dp[0][1] = 2。
    验证:先打印整个区间为 'a'(一次),再打印中间为 'b'(第二次),共2次,正确。
  • 区间 [1,3]: s[1]='b', s[3]='b' 相等,dp[1][3] = dp[1][2] = 2。
  • 区间 [2,4]: s[2]='a', s[4]='a' 相等,dp[2][4] = dp[2][3] = 2。
  • 区间 [0,3]: s[0]='a', s[3]='b' 不同,所以尝试分割点 k=1,2:
    k=1: dp[0][1]+dp[2][3] = 2+2=4
    k=2: dp[0][2]+dp[3][3] = 2+1=3
    取 min=3,所以 dp[0][3]=3。
  • 区间 [1,4]: s[1]='b', s[4]='a' 不同,尝试分割:
    k=2: dp[1][2]+dp[3][4]=2+2=4
    k=3: dp[1][3]+dp[4][4]=2+1=3
    取 min=3,dp[1][4]=3。
  • 区间 [0,4]: s[0]='a', s[4]='a' 相等,所以 dp[0][4] = dp[0][3] = 3。
    验证:先打印整个区间为 'a'(一次),再打印区间 [1,3] 为 'b'(第二次),再打印位置 2 为 'a'(第三次),但这样是3次吗?实际上,最优策略是:先打印整个区间为 'a'(一次),再打印区间 [1,3] 为 'b'(第二次),但此时位置2变成了 'b',需要再打印位置2为 'a'(第三次),共3次。正确。

第六步:算法实现

我们可以用区间 DP 自底向上计算。
伪代码:

n = len(s)
dp = [[0]*n for _ in range(n)]
for i in range(n):
    dp[i][i] = 1
for length in range(2, n+1):
    for i in range(n-length+1):
        j = i+length-1
        dp[i][j] = length  # 最差情况
        if s[i] == s[j]:
            dp[i][j] = dp[i][j-1]  # 注意这里是 j-1,因为 j 可以和 i 一起打印
        else:
            for k in range(i, j):
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j])

但注意,当 s[i]==s[j] 时,我们也可以不单独处理,而在分割中自动优化。但上述处理是常见且正确的。

第七步:复杂度分析

时间复杂度 O(n³),空间复杂度 O(n²)。可以通过进一步优化减少常数,但思路不变。

第八步:总结

这个问题的核心在于:如果区间两端的字符相同,那么可以在一次打印中覆盖两端,从而减少打印次数。通过区间 DP 逐步合并子区间的最优解,最终得到整个字符串的最小打印次数。

奇怪的打印机(最小打印次数问题,进阶版:每次打印必须覆盖一个连续区间,且每次只能打印一种颜色,已打印部分可被覆盖,求打印整个目标字符串的最少打印次数) 题目描述: 给定一个字符串 s,表示最终要打印出的目标字符串。初始时,打印纸是空白的(可以视为由任意数量的相同字符组成,例如全是 'a',但最终我们需要打印出 s)。你有一台奇怪的打印机,它每次操作必须打印一个 连续区间 ,并且这个区间内的所有字符必须被打印成 同一种颜色 (即同一个字符)。已打印的部分可以被后续打印覆盖。问至少需要多少次打印操作,才能得到目标字符串 s。 示例: 输入:s = "aaabbb" 输出:2 解释:先打印整个区间为 'a'(得到 "aaaaaa"),再打印后三个字符为 'b'(得到 "aaabbb")。 解题过程: 这个问题是区间动态规划的典型应用。我们需要逐步推导出最优的打印策略。核心思路是:将字符串分割成子区间,考虑如何在子区间内以最少的打印次数得到目标字符串。 第一步:明确状态定义 定义 dp[i][j] 为打印出子串 s[i...j] (下标从 i 到 j,包含两端)所需的最少打印次数。 目标是求 dp[0][n-1] ,其中 n 是字符串长度。 第二步:分析基础情况 当区间长度为 1 时(即 i == j),只需要打印一次即可得到该字符。 所以 dp[i][i] = 1 。 第三步:状态转移分析(关键) 对于区间 [i, j] ,我们考虑第一次打印操作如何覆盖这个区间。由于每次打印必须是一个连续区间且全部打印为同一种颜色,我们可以考虑第一次打印覆盖了区间 [i, k] ,且这次打印的颜色设为 s[i] (因为最终 s[ i] 的颜色是固定的,第一次打印可以选择打印成 s[ i ],这样后续再覆盖其他部分)。注意,第一次打印不一定只打印一个字符,它可以打印一段连续的区间。 但更通用的思路是: 首先,如果 s[i] == s[j] ,那么我们可以利用一个性质:可以在打印区间 [i, j] 时,先将整个区间打印成 s[i] (或 s[ j]),然后在内部调整。但更精确的优化是:因为 s[ i] 和 s[ j] 相同,我们可以将打印 s[ i] 和 s[ j] 的操作合并考虑,即先打印整个区间为 s[ i],然后再在内部覆盖。但动态规划中更常用的是: 如果 s[ i] == s[ k](i < k <= j),那么可以先打印区间 [ i, k] 为 s[ i],然后问题分解为 [ i+1, k-1] 和 [ k, j] 。但这样处理比较复杂。 更简洁且正确的状态转移方程是: 我们考虑将区间分成两段: [i, k] 和 [k+1, j] 。 如果两段完全独立打印,则总打印次数为 dp[i][k] + dp[k+1][j] 。 但这里有一个优化: 如果 s[ i] 和 s[ k+1] 相同,我们是否可以在某次打印中同时覆盖它们,从而减少一次打印? 实际上,更精确的优化是: 如果 s[ i] == s[ k+1],那么我们可以让第一次打印的颜色为 s[ i],覆盖区间 [ i, m],然后后续处理其他部分。但这样在状态转移中不易直接表示。 经典且正确的状态转移方法是: 对于区间 [i, j] ,我们考虑第一次打印操作打印了区间 [i, k] ,且颜色为 s[i] 。那么这次打印之后,位置 i 已经正确了。接下来我们需要处理剩余部分:区间 [i+1, j] 。但注意,由于第一次打印覆盖了 [ i, k],所以区间 [ i+1, k] 现在也是颜色 s[ i]。如果 s[ i] 和 s[ k+1] 不同,那么 [ i+1, k] 可能需要被覆盖。但更简单的思路是: 将问题分解为 :先打印出 s[ i ] 正确的部分,再处理剩下的。 但最常用的状态转移方程是(经过验证的): dp[i][j] = dp[i][j-1] 如果 s[i] == s[j] 。 为什么?因为如果 s[ i] == s[ j],我们可以在打印 s[ i] 的时候,一次性将整个区间 [ i, j] 打印成 s[ i],然后再在区间内部覆盖修改,而不需要额外增加一次打印来专门处理 s[ j]。但更准确的理解是:我们可以将 s[ j] 视为和 s[ i] 一起打印,所以次数不会比打印 [ i, j-1] 多。但这是否正确?考虑例子 "aba",如果 i=0, j=2, s[ 0]=a, s[ 2]=a,那么 dp[ 0][ 2] 应该等于 2(先打印整个区间为 'a',再打印中间为 'b'),而 dp[ 0][ 1 ] 也是 2(先打印 "aa",再打印第二个为 'b'),所以相等。但注意,这个等式并不总是成立,所以我们需要更精确的推导。 实际上,正确的状态转移方程是: 首先,可以单独打印位置 i,然后处理 [ i+1, j],即 dp[i][j] = 1 + dp[i+1][j] 。 其次,我们尝试节省操作:如果在 [ i+1, j] 中有一个位置 k,使得 s[i] == s[k] ,那么我们可以将区间 [ i, k] 在一次打印中完成(打印成 s[ i]),然后问题分解为:先打印出 [ i+1, k-1] 的部分(因为 [ i, k] 已经打印成 s[ i],所以 [ i+1, k-1] 需要被覆盖成正确的字符),再加上打印 [ k, j] 的部分。但注意,由于 s[ i] == s[ k],打印 [ i, k] 为 s[ i] 后,位置 k 已经是正确的颜色了,所以问题变成:打印 [ i+1, k-1] 和打印 [ k, j] 两部分。但这两部分可能有重叠,所以更好的分解是:打印 [ i+1, k-1] 和打印 [ k+1, j ],但这样不连续。 经过整理,正确的状态转移方程为: dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k+1][j]) 当 s[i] == s[k] 且 i < k <= j。 但这是不完整的,因为当 s[ i] == s[ k] 时,我们可以将区间 [ i, k] 用同一次打印完成,所以总的打印次数可能是:打印 [ i, k] 为 s[ i] 这一次,再加上打印 [ i+1, k-1] 的次数(因为 [ i, k] 已经打印成 s[ i],所以只需要处理内部区间 [ i+1, k-1] 即可),再加上打印 [ k+1, j] 的次数。但注意,打印 [ i+1, k-1] 实际上就是 dp[ i+1][ k-1],而打印 [ k+1, j] 是 dp[ k+1][ j ]。但这样会把区间割裂。 实际上,更标准且正确的状态转移方程是(来自 LeetCode 664 题的标准解法): dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k][j]) 当 s[i] == s[k] 且 i < k <= j。 但这样需要处理边界。常用的正确形式是: dp[i][j] = min(dp[i][j], dp[i][k-1] + dp[k][j] - 1) 如果 s[ i] == s[ k ]。 但这里为了清晰,我们采用另一种更直观的推导。 第四步:标准状态转移方程 定义 dp[i][j] 为打印 s[ i...j ] 的最少打印次数。 基础情况: dp[i][i] = 1 。 对于 i < j : 先考虑最差情况:每次只打印一个字符,则 dp[i][j] = j - i + 1 。 优化:如果 s[i] == s[j] ,那么我们可以将打印 s[ i] 和 s[ j] 合并为同一次操作(在打印区间 [ i, j] 时先整体打印成 s[ i],再处理内部),所以 dp[i][j] = min(dp[i][j], dp[i][j-1]) 。注意,这里 dp[ i][ j-1] 表示打印 s[ i...j-1] 的次数,因为 s[ j] 可以和 s[ i ] 一起打印,所以不会增加次数。 进一步优化:在区间 [ i, j] 中,如果有一个位置 k (i < k <= j) 使得 s[i] == s[k] ,那么我们可以将区间 [ i, k] 在一次操作中打印成 s[ i],然后问题分解为:打印 [ i+1, k-1] 和打印 [ k, j]。但注意,打印 [ k, j] 时,s[ k] 已经正确了,所以实际上只需要打印 [ k, j] 这部分。但更准确的分解是:打印 [ i, k] 为 s[ i] 这一次操作,加上打印 [ i+1, k-1] 的次数,再加上打印 [ k+1, j ] 的次数。但这样比较复杂。 经过验证,标准且正确的状态转移方程为: 但这样在 s[ i] == s[ j] 时,我们可以进一步优化为 dp[i][j] = dp[i][j-1] 。 为什么?因为如果 s[ i] == s[ j],我们可以在打印 s[ i] 的时候,一次性覆盖到 j,这样打印 s[ j] 就不需要额外次数,所以次数等于打印 s[ i...j-1 ] 的次数。 第五步:示例推导 以 s = "ababa" 为例,n=5。 初始化 dp 矩阵,对角线 dp[ i][ i ] = 1。 计算区间长度从 2 到 4(0-indexed)。 区间 [ 0,1]: s[ 0]='a', s[ 1]='b',不相等,所以 dp[ 0][ 1] = min(dp[ 0][ 0]+dp[ 1][ 1 ]) = 1+1=2。 区间 [ 1,2]: 'b' 和 'a' 不同,dp[ 1][ 2 ]=2。 区间 [ 2,3]: 'a' 和 'b' 不同,dp[ 2][ 3 ]=2。 区间 [ 3,4]: 'b' 和 'a' 不同,dp[ 3][ 4 ]=2。 区间 [ 0,2]: s[ 0]='a', s[ 2]='a' 相等,所以 dp[ 0][ 2] = dp[ 0][ 1 ] = 2。 验证:先打印整个区间为 'a'(一次),再打印中间为 'b'(第二次),共2次,正确。 区间 [ 1,3]: s[ 1]='b', s[ 3]='b' 相等,dp[ 1][ 3] = dp[ 1][ 2 ] = 2。 区间 [ 2,4]: s[ 2]='a', s[ 4]='a' 相等,dp[ 2][ 4] = dp[ 2][ 3 ] = 2。 区间 [ 0,3]: s[ 0]='a', s[ 3 ]='b' 不同,所以尝试分割点 k=1,2: k=1: dp[ 0][ 1]+dp[ 2][ 3 ] = 2+2=4 k=2: dp[ 0][ 2]+dp[ 3][ 3 ] = 2+1=3 取 min=3,所以 dp[ 0][ 3 ]=3。 区间 [ 1,4]: s[ 1]='b', s[ 4 ]='a' 不同,尝试分割: k=2: dp[ 1][ 2]+dp[ 3][ 4 ]=2+2=4 k=3: dp[ 1][ 3]+dp[ 4][ 4 ]=2+1=3 取 min=3,dp[ 1][ 4 ]=3。 区间 [ 0,4]: s[ 0]='a', s[ 4]='a' 相等,所以 dp[ 0][ 4] = dp[ 0][ 3 ] = 3。 验证:先打印整个区间为 'a'(一次),再打印区间 [ 1,3] 为 'b'(第二次),再打印位置 2 为 'a'(第三次),但这样是3次吗?实际上,最优策略是:先打印整个区间为 'a'(一次),再打印区间 [ 1,3 ] 为 'b'(第二次),但此时位置2变成了 'b',需要再打印位置2为 'a'(第三次),共3次。正确。 第六步:算法实现 我们可以用区间 DP 自底向上计算。 伪代码: 但注意,当 s[ i]==s[ j ] 时,我们也可以不单独处理,而在分割中自动优化。但上述处理是常见且正确的。 第七步:复杂度分析 时间复杂度 O(n³),空间复杂度 O(n²)。可以通过进一步优化减少常数,但思路不变。 第八步:总结 这个问题的核心在于:如果区间两端的字符相同,那么可以在一次打印中覆盖两端,从而减少打印次数。通过区间 DP 逐步合并子区间的最优解,最终得到整个字符串的最小打印次数。