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

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


题目描述

有一台奇怪的打印机,它每次打印时,必须选择一个连续区间,并将这个区间内的所有字符都统一打印成同一种字母(例如全部打印成 'a''b' 等)。
打印机可以在已打印的字符上重新打印,覆盖之前打印的内容。
给定一个目标字符串 s(只由小写字母组成),问至少需要打印多少次,才能得到这个目标字符串。

示例
输入:s = "aaabbb"
输出:2
解释:

  1. 第一次打印整个区间为 [0,5],全部打印为 'a',得到 "aaaaaa"
  2. 第二次打印区间 [3,5],全部打印为 'b',得到 "aaabbb"

解题思路(区间 DP)

这个问题是区间动态规划的经典题目,核心在于如何利用覆盖操作的性质来设计状态转移。

1. 关键观察

  • 如果一次打印能覆盖一个连续区间,那么这个区间的首尾字符相同时,我们可以用一次打印覆盖整个区间,然后中间部分再调整。
  • 更一般地,如果 s[i] == s[j],那么打印 s[i] 的时候,可以顺便把 s[j] 也打印成同样的字母,然后再处理中间部分。
  • 由于打印是覆盖操作,最后打印的字符决定了这个位置最终是什么字母。

2. 定义状态


dp[i][j] = 打印出子串 s[i..j] 所需的最少打印次数。

其中 ij 是闭区间索引,0 <= i <= j < n


3. 边界条件

  • 当区间长度为 1 时,只需打印一次:dp[i][i] = 1

4. 状态转移方程推导

考虑区间 [i, j]

情况 1:s[i] == s[j]

  • 我们可以在第一次打印时,将整个区间 [i, j] 打印成 s[i](即 s[j])。

  • 之后,我们只需处理区间 [i, j-1] 或者 [i+1, j] 的细节,因为 s[j] 已经在第一次打印时正确了。

  • 更精确地说:
    第一次打印区间 [i, j]s[i] 后,s[i]s[j] 都已经正确,但中间可能还需要调整。
    这时,相当于在 [i, j] 区间内,先保证 s[i] 正确,然后处理 [i+1, j],但要注意 s[i]s[j] 相同,所以可以节省一次对 s[j] 的额外打印。
    实际上,可以这样理解:
    dp[i][j] = dp[i][j-1]
    因为最后打印 s[i] 时可以覆盖到 s[j],所以 s[j] 不用单独处理。

    但这是不完全正确的,因为可能中间有和 s[i] 不同的字符,我们还要处理它们。
    更准确的标准推导:
    假设第一次打印是打印整个区间为 s[i],那么 s[j] 也正确了,接下来只需要处理 [i, j-1] 区间,但 [i, j-1] 的首字符已经是 s[i] 正确,所以其实等价于处理 [i, j-1] 区间,但要注意 s[i] 可能和中间某些字符相同,可以减少打印次数。
    实际上,更通用的正确方程是:
    如果 s[i] == s[j],则 dp[i][j] = dp[i][j-1]
    为什么?
    因为要得到 s[i..j],我们可以先得到 s[i..j-1],然后在最后一步打印区间 [i, j]s[i](即 s[j]),这样 s[j] 就会从原来的(可能是错的)变成正确的,而且这一步不增加打印次数,因为它可以合并到某次打印中吗?
    实际上,我们可以在得到 s[i..j-1] 的过程中,最后一次打印 s[i] 时直接延伸到 j,这样打印 s[i..j] 的总次数等于打印 s[i..j-1] 的次数。

    严谨地说:
    构造 s[i..j] 时,可以让第一次打印覆盖 [i, k]s[i],其中 k 是某个位置。但更简单的推导是:
    考虑 s[i] == s[j],那么我们可以先打印整个区间为 s[i],这算一次打印,然后我们只需处理 [i+1, j] 区间,但这样会多算吗?
    实际上标准做法是:
    dp[i][j] = min(dp[i][j-1], dp[i+1][j])s[i] == s[j] 时。
    为什么?
    如果第一次打印时覆盖了 [i, j]s[i],那么 s[j] 已经正确,我们只需处理 [i, j-1],但 s[i] 也正确,所以问题变为 dp[i][j-1]
    同理,也可以先打印整个区间为 s[j],然后处理 [i+1, j],即 dp[i+1][j]
    因此:
    转移 1dp[i][j] = min(dp[i][j-1], dp[i+1][j]) 如果 s[i] == s[j]

情况 2:s[i] != s[j]

  • 这时,区间首尾字符不同,无法在一次打印中同时让它们都正确(因为一次打印只能是一种字母)。
  • 我们需要在中间某个位置 k 将区间分割成两部分分别处理:
    dp[i][j] = min(dp[i][k] + dp[k+1][j]),其中 kij-1

5. 整合方程

综合起来:

  1. 初始化 dp[i][i] = 1
  2. 对于长度 len 从 2 到 n:
    • 对于每个 ij = i + len - 1
      • 如果 s[i] == s[j]
        dp[i][j] = dp[i][j-1] 或者 dp[i+1][j] 的最小值。
        (其实 dp[i][j-1]dp[i+1][j] 是相等的吗?不一定,取最小值即可。)
      • 如果 s[i] != s[j]
        dp[i][j] = min(dp[i][k] + dp[k+1][j])k = i..j-1

但更常见且正确的写法(也是标准答案的写法):

  • 先令 dp[i][j] = j - i + 1(最差情况,每个字符单独打印)。
  • 如果 s[i] == s[j],则 dp[i][j] = dp[i][j-1]
  • 然后枚举中间点 k 进行分割:dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j])

注意:当 s[i] == s[j] 时,dp[i][j] = dp[i][j-1] 是成立的,因为可以在最后一步顺带打印 s[j] 而不增加次数。但为了保险,也常直接按 dp[i][j] = min(dp[i][j-1], dp[i+1][j]) 处理。

经过检验,标准正确方程是:

dp[i][j] = dp[i][j-1]                     if s[i] == s[j]
dp[i][j] = min(dp[i][k] + dp[k+1][j])    for k in [i, j-1]  otherwise

但枚举分割点时,即使 s[i] == s[j] 也可能通过分割得到更优解,所以实际代码中,我们总是枚举分割点,但初始化时如果 s[i] == s[j] 可以先设为 dp[i][j-1] 作为候选。


6. 举例推导

s = "aba"

  • dp[0][0]=1, dp[1][1]=1, dp[2][2]=1
  • 区间 [0,1]s[0]='a', s[1]='b',不同,枚举 k=0
    dp[0][1] = dp[0][0]+dp[1][1] = 1+1=2
  • 区间 [1,2]s[1]='b', s[2]='a',不同,枚举 k=1
    dp[1][2] = dp[1][1]+dp[2][2] = 2
  • 区间 [0,2]s[0]='a', s[2]='a',相同,先设 dp[0][2] = dp[0][1] = 2
    再枚举分割点:
    • k=0dp[0][0]+dp[1][2]=1+2=3
    • k=1dp[0][1]+dp[2][2]=2+1=3
      最小是 2,所以 dp[0][2]=2

结果:最少打印 2 次。
解释:第一次打印整个区间为 'a' 得到 "aaa",第二次打印区间 [1,1]'b' 得到 "aba"


7. 算法步骤

  1. 初始化 dp[n][n] 为 0,dp[i][i]=1
  2. 按区间长度从小到大计算:
    • 对每个 i,j,先设 dp[i][j] = j-i+1
    • 如果 s[i] == s[j],则 dp[i][j] = dp[i][j-1](当 j>i 时,注意 len=2dp[i][i] 已知)。
    • 枚举中间分割点 kij-1
      dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j])
  3. 返回 dp[0][n-1]

8. 复杂度

  • 时间复杂度:O(n³),三层循环(长度、起点、分割点)。
  • 空间复杂度:O(n²)。

9. 示例代码(Python 风格)

def strangePrinter(s: str) -> int:
    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]
            for k in range(i, j):
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j])
    return dp[0][n - 1] if n > 0 else 0
奇怪的打印机的最小打印次数问题(每次打印必须覆盖一个连续区间,且每次只能打印一种颜色,已打印部分可被覆盖,求打印整个目标字符串的最少打印次数) 题目描述 有一台奇怪的打印机,它每次打印时,必须选择一个 连续区间 ,并将这个区间内的所有字符都 统一打印成同一种字母 (例如全部打印成 'a' 或 'b' 等)。 打印机可以在已打印的字符上 重新打印 ,覆盖之前打印的内容。 给定一个目标字符串 s (只由小写字母组成),问至少需要 打印多少次 ,才能得到这个目标字符串。 示例 输入: s = "aaabbb" 输出: 2 解释: 第一次打印整个区间为 [0,5] ,全部打印为 'a' ,得到 "aaaaaa" 。 第二次打印区间 [3,5] ,全部打印为 'b' ,得到 "aaabbb" 。 解题思路(区间 DP) 这个问题是 区间动态规划 的经典题目,核心在于如何利用 覆盖操作 的性质来设计状态转移。 1. 关键观察 如果一次打印能覆盖一个连续区间,那么 这个区间的首尾字符相同 时,我们可以用一次打印覆盖整个区间,然后中间部分再调整。 更一般地,如果 s[i] == s[j] ,那么打印 s[i] 的时候,可以顺便把 s[j] 也打印成同样的字母,然后再处理中间部分。 由于打印是覆盖操作, 最后打印的字符 决定了这个位置最终是什么字母。 2. 定义状态 设 dp[i][j] = 打印出子串 s[i..j] 所需的最少打印次数。 其中 i 和 j 是闭区间索引, 0 <= i <= j < n 。 3. 边界条件 当区间长度为 1 时,只需打印一次: dp[i][i] = 1 。 4. 状态转移方程推导 考虑区间 [i, j] : 情况 1: s[i] == s[j] 我们可以在第一次打印时,将整个区间 [i, j] 打印成 s[i] (即 s[j] )。 之后,我们只需处理区间 [i, j-1] 或者 [i+1, j] 的细节,因为 s[j] 已经在第一次打印时正确了。 更精确地说: 第一次打印区间 [i, j] 为 s[i] 后, s[i] 和 s[j] 都已经正确,但中间可能还需要调整。 这时, 相当于在 [i, j] 区间内,先保证 s[i] 正确,然后处理 [i+1, j] ,但要注意 s[i] 和 s[j] 相同,所以可以节省一次对 s[j] 的额外打印。 实际上,可以这样理解: dp[i][j] = dp[i][j-1] 因为最后打印 s[i] 时可以覆盖到 s[j] ,所以 s[j] 不用单独处理。 但这是 不完全正确 的,因为可能中间有和 s[i] 不同的字符,我们还要处理它们。 更准确的标准推导: 假设第一次打印是打印整个区间为 s[i] ,那么 s[j] 也正确了,接下来只需要处理 [i, j-1] 区间,但 [i, j-1] 的首字符已经是 s[i] 正确,所以其实等价于处理 [i, j-1] 区间,但要注意 s[i] 可能和中间某些字符相同,可以减少打印次数。 实际上,更通用的正确方程是: 如果 s[i] == s[j] ,则 dp[i][j] = dp[i][j-1] 。 为什么? 因为要得到 s[i..j] ,我们可以先得到 s[i..j-1] ,然后在最后一步打印区间 [i, j] 为 s[i] (即 s[j] ),这样 s[j] 就会从原来的(可能是错的)变成正确的,而且这一步不增加打印次数,因为它可以合并到某次打印中吗? 实际上,我们可以在得到 s[i..j-1] 的过程中,最后一次打印 s[i] 时直接延伸到 j ,这样打印 s[i..j] 的总次数等于打印 s[i..j-1] 的次数。 严谨地说: 构造 s[i..j] 时,可以让第一次打印覆盖 [i, k] 为 s[i] ,其中 k 是某个位置。但更简单的推导是: 考虑 s[i] == s[j] ,那么我们可以 先打印整个区间为 s[i] ,这算一次打印,然后我们只需处理 [i+1, j] 区间,但这样会多算吗? 实际上标准做法是: dp[i][j] = min(dp[i][j-1], dp[i+1][j]) 当 s[i] == s[j] 时。 为什么? 如果第一次打印时覆盖了 [i, j] 为 s[i] ,那么 s[j] 已经正确,我们只需处理 [i, j-1] ,但 s[i] 也正确,所以问题变为 dp[i][j-1] 。 同理,也可以先打印整个区间为 s[j] ,然后处理 [i+1, j] ,即 dp[i+1][j] 。 因此: 转移 1 : dp[i][j] = min(dp[i][j-1], dp[i+1][j]) 如果 s[i] == s[j] 。 情况 2: s[i] != s[j] 这时,区间首尾字符不同,无法在一次打印中同时让它们都正确(因为一次打印只能是一种字母)。 我们需要在中间某个位置 k 将区间分割成两部分分别处理: dp[i][j] = min(dp[i][k] + dp[k+1][j]) ,其中 k 从 i 到 j-1 。 5. 整合方程 综合起来: 初始化 dp[i][i] = 1 。 对于长度 len 从 2 到 n: 对于每个 i , j = i + len - 1 : 如果 s[i] == s[j] : dp[i][j] = dp[i][j-1] 或者 dp[i+1][j] 的最小值。 (其实 dp[i][j-1] 和 dp[i+1][j] 是相等的吗?不一定,取最小值即可。) 如果 s[i] != s[j] : dp[i][j] = min(dp[i][k] + dp[k+1][j]) 对 k = i..j-1 。 但更常见且正确的写法(也是标准答案的写法): 先令 dp[i][j] = j - i + 1 (最差情况,每个字符单独打印)。 如果 s[i] == s[j] ,则 dp[i][j] = dp[i][j-1] 。 然后枚举中间点 k 进行分割: dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j]) 。 注意 :当 s[i] == s[j] 时, dp[i][j] = dp[i][j-1] 是成立的,因为可以在最后一步顺带打印 s[j] 而不增加次数。但为了保险,也常直接按 dp[i][j] = min(dp[i][j-1], dp[i+1][j]) 处理。 经过检验, 标准正确方程 是: 但枚举分割点时,即使 s[i] == s[j] 也可能通过分割得到更优解,所以实际代码中,我们总是枚举分割点,但初始化时如果 s[i] == s[j] 可以先设为 dp[i][j-1] 作为候选。 6. 举例推导 s = "aba" dp[0][0]=1 , dp[1][1]=1 , dp[2][2]=1 。 区间 [0,1] : s[0]='a' , s[1]='b' ,不同,枚举 k=0 : dp[0][1] = dp[0][0]+dp[1][1] = 1+1=2 。 区间 [1,2] : s[1]='b' , s[2]='a' ,不同,枚举 k=1 : dp[1][2] = dp[1][1]+dp[2][2] = 2 。 区间 [0,2] : s[0]='a' , s[2]='a' ,相同,先设 dp[0][2] = dp[0][1] = 2 。 再枚举分割点: k=0 : dp[0][0]+dp[1][2]=1+2=3 k=1 : dp[0][1]+dp[2][2]=2+1=3 最小是 2,所以 dp[0][2]=2 。 结果:最少打印 2 次。 解释:第一次打印整个区间为 'a' 得到 "aaa" ,第二次打印区间 [1,1] 为 'b' 得到 "aba" 。 7. 算法步骤 初始化 dp[n][n] 为 0, dp[i][i]=1 。 按区间长度从小到大计算: 对每个 i,j ,先设 dp[i][j] = j-i+1 。 如果 s[i] == s[j] ,则 dp[i][j] = dp[i][j-1] (当 j>i 时,注意 len=2 时 dp[i][i] 已知)。 枚举中间分割点 k 从 i 到 j-1 : dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j]) 。 返回 dp[0][n-1] 。 8. 复杂度 时间复杂度:O(n³),三层循环(长度、起点、分割点)。 空间复杂度:O(n²)。 9. 示例代码(Python 风格)