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

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


题目描述

你有一台奇怪的打印机,它有两个特殊规则:

  1. 每次打印必须选择一个连续区间 [i, j],并将这个区间内所有字符都打印成同一种颜色(用一个小写字母表示)。
  2. 打印可以覆盖之前已经打印过的字符(即可以修改之前打印的内容)。

给你一个目标字符串 s,问至少需要多少次打印,才能打印出整个目标字符串 s

示例

  • 输入:s = "aaabbb"
    输出:2
    解释:第一次打印整个区间为 'a',得到 "aaaaaa",第二次打印区间 [3,5]'b',得到 "aaabbb"
  • 输入:s = "aba"
    输出:2
    解释:第一次打印整个区间为 'a',得到 "aaa",第二次打印区间 [1,1](下标从 0 开始)为 'b',得到 "aba"

注意:打印次数必须最少,打印的字母必须与目标字符串对应位置最终一致。


问题分析

这是一个区间动态规划问题。

  • 如果我们直接考虑从左到右打印,可能会因为后续需要覆盖而难以决策。
  • 关键观察:
    1. 如果 s[i] == s[j],那么打印 s[i..j] 时,可以在打印 s[i] 的时候顺便把 s[j] 也打印成相同颜色,而不增加额外次数。
    2. 如果我们先打印一个区间,内部可以分成两个子区间分别打印,但要注意首尾字符相同时的优化。

动态规划定义

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

  • 初始情况
    i == j 时,只需要打印一次,即 dp[i][i] = 1

  • 一般情况i < j):
    我们可以将 s[i..j] 分成两部分 s[i..k]s[k+1..j] 分别打印,那么:

    dp[i][j] = min(dp[i][k] + dp[k+1][j]),其中 i ≤ k < j
    

    但这样没有考虑首尾字符相同时的优化。


关键优化

如果 s[i] == s[j],那么打印 s[i] 的时候可以顺便把 s[j] 打印成相同字母。
具体来说:

  • 我们可以第一次打印时就把 s[i] 打印到整个区间,然后在内部调整。
  • 更精确的说法是:
    当我们打印 s[i](颜色与目标一致)时,如果 s[i] == s[j],那么 s[j] 也可以在这一次打印中被涂成正确颜色(之后再覆盖中间部分)。
    因此,我们可以将问题转化为先打印 s[i..j-1]s[i+1..j],因为 s[j] 已经被顺便打印正确。

实际上更通用的转移:
如果 s[i] == s[j],那么 dp[i][j] = dp[i][j-1]
为什么
考虑打印 s[i..j]

  1. 先打印 s[i..j-1],需要 dp[i][j-1] 次。
  2. 因为 s[i] == s[j],在打印 s[i] 的那一次操作中,我们可以把 s[j] 也一起打印成 s[i](即正确颜色),而不需要额外次数。
  3. 但要注意:打印 s[i..j-1] 的过程中,可能 s[j] 被中间操作覆盖过,所以需要保证最后 s[j] 正确。
    实际上,我们可以将第一次打印 s[i] 的操作延伸到 j,这样 s[j] 一开始就是对的,之后打印内部时再覆盖中间部分。
    数学上可以证明,这样等价于 dp[i][j] = dp[i][j-1]

所以转移方程为:

如果 s[i] == s[j]:
    dp[i][j] = dp[i][j-1]
否则:
    dp[i][j] = min(dp[i][k] + dp[k+1][j]),其中 i ≤ k < j

计算顺序

区间 DP 一般按长度从小到大计算:

  1. 先初始化长度为 1 的区间。
  2. 然后长度从 2 到 n 枚举。
  3. 对于每个区间 [i, j],按上述转移计算。

示例推导

s = "aba" 为例:

  1. 初始化:
    dp[0][0] = 1
    dp[1][1] = 1
    dp[2][2] = 1

  2. 长度 2 区间:

    • [0,1]s[0]='a', s[1]='b',不相同
      枚举 k=0dp[0][0]+dp[1][1]=1+1=2
      所以 dp[0][1]=2
    • [1,2]s[1]='b', s[2]='a',不相同
      枚举 k=1dp[1][1]+dp[2][2]=1+1=2
      所以 dp[1][2]=2
  3. 长度 3 区间 [0,2]
    s[0]='a', s[2]='a',相同
    所以 dp[0][2] = dp[0][1] = 2
    验证:
    第一次打印整个区间为 'a',得 "aaa"
    第二次打印区间 [1,1]'b',得 "aba",共 2 次。

输出 dp[0][n-1] = 2


代码框架(伪代码)

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
            if s[i] == s[j]:
                dp[i][j] = dp[i][j-1]
            else:
                dp[i][j] = float('inf')
                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]

时间复杂度与优化

  • 朴素实现:三重循环,O(n^3)
  • 可优化:当 s[i] == s[j] 时,已经 O(1) 转移;不同时才需要枚举分割点。
  • 对于某些特殊数据,可以进一步优化枚举范围,但最坏仍 O(n^3),在 n ≤ 100 时可行。

总结

这道题的核心在于发现:

  1. 首尾字符相同时,尾部字符可被“免费”打印,从而减少一次操作。
  2. 将区间分成两半分别打印,是区间 DP 的常见思路。

通过以上分析,我们可以用 dp[i][j] 表示子串的最少打印次数,并给出转移方程,最终得到整个字符串的最小打印次数。

奇怪的打印机(最小打印次数问题,每次打印必须覆盖一个连续区间,且每次只能打印一种颜色,已打印部分可被覆盖,求打印整个目标字符串的最少打印次数) 题目描述 你有一台奇怪的打印机,它有两个特殊规则: 每次打印必须选择一个连续区间 [i, j] ,并将这个区间内所有字符都打印成 同一种颜色 (用一个小写字母表示)。 打印可以覆盖之前已经打印过的字符 (即可以修改之前打印的内容)。 给你一个目标字符串 s ,问 至少需要多少次打印 ,才能打印出整个目标字符串 s 。 示例 : 输入: s = "aaabbb" 输出: 2 解释:第一次打印整个区间为 'a' ,得到 "aaaaaa" ,第二次打印区间 [3,5] 为 'b' ,得到 "aaabbb" 。 输入: s = "aba" 输出: 2 解释:第一次打印整个区间为 'a' ,得到 "aaa" ,第二次打印区间 [1,1] (下标从 0 开始)为 'b' ,得到 "aba" 。 注意 :打印次数必须最少,打印的字母必须与目标字符串对应位置最终一致。 问题分析 这是一个 区间动态规划 问题。 如果我们直接考虑从左到右打印,可能会因为后续需要覆盖而难以决策。 关键观察: 如果 s[i] == s[j] ,那么打印 s[i..j] 时,可以在打印 s[i] 的时候顺便把 s[j] 也打印成相同颜色,而不增加额外次数。 如果我们先打印一个区间,内部可以分成两个子区间分别打印,但要注意 首尾字符相同 时的优化。 动态规划定义 设 dp[i][j] 表示打印出子串 s[i..j] 所需的最少打印次数。 初始情况 : 当 i == j 时,只需要打印一次,即 dp[i][i] = 1 。 一般情况 ( i < j ): 我们可以将 s[i..j] 分成两部分 s[i..k] 和 s[k+1..j] 分别打印,那么: 但这样没有考虑 首尾字符相同 时的优化。 关键优化 如果 s[i] == s[j] ,那么打印 s[i] 的时候可以顺便把 s[j] 打印成相同字母。 具体来说: 我们可以 第一次打印时就把 s[i] 打印到整个区间 ,然后在内部调整。 更精确的说法是: 当我们打印 s[i] (颜色与目标一致)时,如果 s[i] == s[j] ,那么 s[j] 也可以在这一次打印中被涂成正确颜色(之后再覆盖中间部分)。 因此,我们可以 将问题转化为先打印 s[i..j-1] 或 s[i+1..j] ,因为 s[j] 已经被顺便打印正确。 实际上更通用的转移: 如果 s[i] == s[j] ,那么 dp[i][j] = dp[i][j-1] 。 为什么 ? 考虑打印 s[i..j] : 先打印 s[i..j-1] ,需要 dp[i][j-1] 次。 因为 s[i] == s[j] ,在打印 s[i] 的那一次操作中,我们可以把 s[j] 也一起打印成 s[i] (即正确颜色),而不需要额外次数。 但要注意:打印 s[i..j-1] 的过程中,可能 s[j] 被中间操作覆盖过,所以需要保证最后 s[j] 正确。 实际上,我们可以将第一次打印 s[i] 的操作延伸到 j ,这样 s[j] 一开始就是对的,之后打印内部时再覆盖中间部分。 数学上可以证明,这样等价于 dp[i][j] = dp[i][j-1] 。 所以转移方程为: 计算顺序 区间 DP 一般按长度从小到大计算: 先初始化长度为 1 的区间。 然后长度从 2 到 n 枚举。 对于每个区间 [i, j] ,按上述转移计算。 示例推导 以 s = "aba" 为例: 初始化: dp[0][0] = 1 dp[1][1] = 1 dp[2][2] = 1 长度 2 区间: [0,1] : s[0]='a' , s[1]='b' ,不相同 枚举 k=0 : dp[0][0]+dp[1][1]=1+1=2 所以 dp[0][1]=2 [1,2] : s[1]='b' , s[2]='a' ,不相同 枚举 k=1 : dp[1][1]+dp[2][2]=1+1=2 所以 dp[1][2]=2 长度 3 区间 [0,2] : s[0]='a' , s[2]='a' ,相同 所以 dp[0][2] = dp[0][1] = 2 。 验证: 第一次打印整个区间为 'a' ,得 "aaa" 第二次打印区间 [1,1] 为 'b' ,得 "aba" ,共 2 次。 输出 dp[0][n-1] = 2 。 代码框架(伪代码) 时间复杂度与优化 朴素实现:三重循环, O(n^3) 。 可优化:当 s[i] == s[j] 时,已经 O(1) 转移;不同时才需要枚举分割点。 对于某些特殊数据,可以进一步优化枚举范围,但最坏仍 O(n^3) ,在 n ≤ 100 时可行。 总结 这道题的核心在于发现: 首尾字符相同时,尾部字符可被“免费”打印 ,从而减少一次操作。 将区间分成两半分别打印,是区间 DP 的常见思路。 通过以上分析,我们可以用 dp[i][j] 表示子串的最少打印次数,并给出转移方程,最终得到整个字符串的最小打印次数。