奇怪的打印机 II
字数 2690 2025-10-28 20:05:14

奇怪的打印机 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. 初始化所有长度为 1 的区间 dp[i][i] = 1。
  2. 然后计算长度为 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。
  • 计算长度为 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,与题目例子一致。

总结
这个问题的核心在于识别当区间两端的字符相同时,可以减少一次打印操作的可能性,同时结合区间分割的经典动态规划思想。通过自底向上地计算所有小区间的最优解,最终得到整个字符串的最少打印次数。

奇怪的打印机 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。 计算长度为 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,与题目例子一致。 总结 这个问题的核心在于识别当区间两端的字符相同时,可以减少一次打印操作的可能性,同时结合区间分割的经典动态规划思想。通过自底向上地计算所有小区间的最优解,最终得到整个字符串的最少打印次数。