奇怪的打印机(最小打印次数问题,每次打印必须覆盖一个连续区间,且每次只能打印一种颜色,已打印部分可被覆盖,求打印整个目标字符串的最少打印次数)
题目描述
你有一台奇怪的打印机,它有两个特殊规则:
- 每次打印必须选择一个连续区间
[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]分别打印,那么: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]:
- 先打印
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]。
所以转移方程为:
如果 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 的区间。
- 然后长度从 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。
代码框架(伪代码)
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时可行。
总结
这道题的核心在于发现:
- 首尾字符相同时,尾部字符可被“免费”打印,从而减少一次操作。
- 将区间分成两半分别打印,是区间 DP 的常见思路。
通过以上分析,我们可以用 dp[i][j] 表示子串的最少打印次数,并给出转移方程,最终得到整个字符串的最小打印次数。