奇怪的打印机 II
题目描述
有一个奇怪的打印机,它每次打印时可以选择打印一段连续的相同字符。打印机会用一种特殊的墨水,每次打印会覆盖原来位置上可能存在的字符。现在给定一个字符串 s,表示打印机最终打印出的结果。你的任务是找出打印机打印出字符串 s 所需的最少打印次数。
例如,对于字符串 s = "aaabbb":
- 一种方法是先打印出 "aaa"(一次),然后打印出 "bbb"(第二次),总共需要 2 次。
- 另一种方法是先打印出整个字符串 "aaabbb"(一次),但这样第一次打印的字符必须全部相同,而 'a' 和 'b' 不同,所以此方法不可行。最少打印次数为 2。
对于字符串 s = "aba":
- 如果先打印出 "aaa"(一次),然后在第二个位置打印 'b'(第二次),这样会覆盖掉原来的 'a',得到 "aba"。但注意,第二次打印 'b' 时,是连续打印一个字符 'b',它覆盖了第二个位置的字符。总共需要 2 次。
- 实际上,最少打印次数是 2:先打印整个字符串为 "aaa"(一次),然后打印中间字符为 'b'(第二次);或者先打印整个字符串为 "bbb"(一次),然后打印首尾字符为 'a'(第二次)。
解题过程
这是一个典型的区间动态规划问题。我们可以定义 dp[i][j] 表示打印出字符串 s 中从索引 i 到索引 j(包含)的子串所需的最少打印次数。
1. 状态定义
- dp[i][j]: 打印子串 s[i...j] 所需的最少打印次数。
2. 边界情况
- 当区间长度为 1 时,即 i == j,只需要一次打印即可完成(打印一个字符)。所以 dp[i][i] = 1。
3. 状态转移方程
考虑一般情况,区间 [i, j]。
-
情况1: 如果 s[i] == s[j]。
我们可以设想,打印 s[i] 和 s[j] 的那次操作可以是同一次打印操作的一部分(即使中间有其他字符)。例如 "abaca",首尾都是 'a',那么第一次打印可以打印出整个区间的底色(全部打印成 'a'),然后再去处理中间的部分。但这并不意味着 dp[i][j] 直接等于 dp[i][j-1] 或 dp[i+1][j],因为可能中间部分和首尾的 'a' 一起打印会更优。
一个更通用的思路是:当 s[i] == s[j] 时,打印 s[i] 的操作可以“顺便”帮助打印 s[j],反之亦然。实际上,我们可以将区间视为在 i 和 j 处用同一种颜色打底,那么打印 s[i...j] 的次数可能等于打印 s[i...j-1] 的次数(因为 j 处的字符可以在打印 i 处字符时一起完成)。但更准确的处理是:dp[i][j] = dp[i][j-1]。为什么?因为当我们打印 s[i](这个字符和 s[j] 相同)时,我们可以将这次打印操作延伸到 j 位置,从而“免费”覆盖掉 j 位置。所以打印 [i, j] 并不比打印 [i, j-1] 需要更多次数。当然,对称地,dp[i][j] = dp[i+1][j] 也是成立的。为了得到最小值,我们通常取 min(dp[i][j-1], dp[i+1][j])。但更常见的处理是直接令 dp[i][j] = dp[i][j-1](或 dp[i+1][j]),因为在状态转移过程中,我们还会考虑其他情况,最终取最小值。 -
情况2: 无论 s[i] 和 s[j] 是否相等,我们都需要考虑将区间 [i, j] 分割成两个部分 [i, k] 和 [k+1, j] (i <= k < j),然后分别打印这两个部分。总打印次数为两部分打印次数之和,即 dp[i][k] + dp[k+1][j]。我们需要遍历所有可能的分割点 k,找到使得总打印次数最小的那个分割方案。
因此,综合起来,状态转移方程为:
dp[i][j] = min( dp[i][j], dp[i][k] + dp[k+1][j] ) 对于所有 k 在 [i, j-1] 范围内。
并且,如果 s[i] == s[j],我们还有一个可能更优的选择:dp[i][j] = min( dp[i][j], dp[i][j-1] ) (或者 dp[i+1][j])。
4. 计算顺序
由于动态规划的状态 dp[i][j] 依赖于更短的区间(即 i 更大、j 更小的状态还未计算),我们需要按区间长度从小到大的顺序进行计算。
- 初始化所有长度为 1 的区间 dp[i][i] = 1。
- 然后计算长度为 2 的区间,接着长度 3,...,直到长度 n(整个字符串)。
5. 举例说明
以 s = "aba" 为例。
- n = 3。
- 初始化:
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。
- [0,1]: s[0]='a', s[1]='b', 不相等。
- 计算长度为 3 的区间 [0,2]:
s[0]='a', s[2]='a', 相等。所以我们可以先考虑情况1:dp[0][2] 可能等于 dp[0][1] 或 dp[1][2]。
尝试 dp[0][1] = 2。
尝试 dp[1][2] = 2。
情况2:遍历分割点 k。
k=0: dp[0][0] + dp[1][2] = 1 + 2 = 3。
k=1: dp[0][1] + dp[2][2] = 2 + 1 = 3。
取最小值 min(2, 3, 3) = 2。
所以 dp[0][2] = 2。
最终结果为 dp[0][2] = 2,与题目例子一致。
总结
这个问题的核心在于识别当区间两端的字符相同时,可以减少一次打印操作的可能性,同时结合区间分割的经典动态规划思想。通过自底向上地计算所有小区间的最优解,最终得到整个字符串的最少打印次数。