奇怪的打印机最小打印次数问题(每次打印一个连续区间,可覆盖,但每次打印的颜色必须相同,且目标串由两种颜色字符组成)
题目描述
有一台奇怪的打印机,它有以下打印规则:
- 打印机每次可以打印一个任意长度的连续区间。
- 每次打印只能使用一种颜色。
- 打印机会用当前颜色完全覆盖所选区间内已有的任何字符(即可以覆盖之前的打印)。
- 打印机最初在空白纸上开始,最终需要打印出一个由两种字符(例如
'A'和'B')组成的长度为n的目标字符串s。 - 你需要求出打印出恰好为目标串
s的最少打印次数。
例如,目标串 s = "ABAB"。
- 一种方案:先打印整个区间为
'A',得到"AAAA",然后打印第2、4个位置为'B',需要3次。 - 更优方案:先打印整个区间为
'A',得到"AAAA",然后打印区间[2,3]为'B',得到"ABBA",最后打印第4个位置为'B',还是3次。 - 最优方案:先打印第1个位置为
'A',然后打印第2个位置为'B',然后打印第3个位置为'A',最后打印第4个位置为'B',需要4次?不对,这不如前面的3次。 - 实际上最优是:先打印整个区间为
'A',得到"AAAA",然后打印区间[2,3]为'B',得到"ABBA",此时第4个位置还是'A',与目标'B'不符,所以再打印第4个位置为'B',总共3次。
但更聪明的办法:先打印整个区间为 'A',得到 "AAAA",然后打印区间 [2,4] 为 'B',直接得到 "ABBB"?不对,目标 [3] 是 'A',所以不行。
实际上,最优是 2 次:先打印整个区间为 'A',然后打印区间 [2,4] 为 'B',得到 "ABBB",但目标 [3] 是 'A',所以不对。
所以 "ABAB" 的最优是 3 次。
这个题目是原“奇怪的打印机”问题的简化版本,因为只有两种颜色,我们可以利用这个特性简化状态设计。
解题思路
原“奇怪的打印机”问题(字符集为26个小写字母)的状态转移为:
设 dp[i][j] 表示打印出区间 s[i...j] 所需的最少打印次数。
转移时,考虑第一次打印区间 [i, k] 的颜色为 s[i],然后递归处理剩余部分,但这样可能不是最优,因为第一次打印的颜色可能不是 s[i]。
标准解法是:
- 如果
s[i] == s[j],则可以在打印s[i]时顺带覆盖到j,所以dp[i][j] = dp[i][j-1]。 - 否则,枚举分割点
k,将区间分成[i, k]和[k+1, j]分别打印,取最小值。
但是对于只有两种颜色的情况,我们可以进一步优化思路。
关键观察
因为只有 'A' 和 'B' 两种字符,我们可以定义:
dp[i][j][c] 表示将区间 [i, j] 打印成全部为字符 c 的最少打印次数。
但我们需要的是最终与目标一致,而不是全部相同。
我们可以这样想:最终打印结果必须与目标串一致。我们可以逆向思考,从空纸开始,每次打印一个连续区间为一种颜色,覆盖之前的颜色。等价于从目标串开始,用最少的区间覆盖它,使得每个区间内颜色相同,且区间的并覆盖整个串。但是,覆盖时如果区间重叠,后打印的会覆盖先打印的,所以我们要考虑打印顺序。
更直观的DP设计:
设 f[i][j] 表示打印出区间 [i, j] 与目标一致的最少打印次数。
转移方程:
- 如果
s[i] == s[j],那么我可以在打印s[i]的时候一次性覆盖到j(因为中间可以覆盖成别的再改回来?不对)。实际上,如果首尾字符相同,最优策略中第一次打印区间[i, j]为s[i]可能是最优的,这样剩下的部分只需要在内部调整。
更精确地:f[i][j] = f[i][j-1]。
为什么?因为打印完区间[i, j-1]后,最后一个字符s[j]与s[i]相同,所以可以在某次打印s[i]时顺带覆盖到j,而不增加次数。 - 如果
s[i] != s[j],那么区间必须分成两段分别打印,因为不可能一次打印同时满足首尾不同字符。枚举分割点k:
f[i][j] = min{f[i][k] + f[k+1][j]},其中i ≤ k < j。
这个转移方程与原版奇怪打印机完全相同,但我们可以利用两种颜色特性来简化某些情况吗?实际上,状态转移方程是通用的,两种颜色并不会减少DP的复杂度,但可以帮助我们更好地理解。
详细步骤
1. 定义状态
dp[i][j]:打印出子串 s[i...j](下标从 i 到 j)所需的最少打印次数。
2. 初始化
- 长度为1的区间:
dp[i][i] = 1,一次打印一个字符即可。 - 其他区间初始化为无穷大。
3. 状态转移
遍历区间长度 len 从 2 到 n,遍历起点 i,计算终点 j = i + len - 1。
-
情况1:
s[i] == s[j]
可以在打印s[i]时顺带覆盖到j(不一定是第一次打印,但可以在某次打印s[i]时延伸到j),因此dp[i][j] = dp[i][j-1]。
例如"A...A",打印完[i, j-1]后,再打印一次s[i]覆盖到j即可,不增加次数。
但要注意:这是最优的吗?考虑"ABBA",s[0]='A',s[3]='A',那么dp[0][3] = dp[0][2],而dp[0][2]打印"ABB"需要2次,再加一次打印'A'覆盖最后一个?不对,这里逻辑要仔细:
实际上,s[i]==s[j]时,第一次打印区间[i, j]为s[i],然后内部可能需要修改,但修改时可以用别的颜色覆盖,最后再局部调整。等价于打印区间[i, j-1]后,最后一个字符可以通过某次打印s[i]时一起完成。
标准转移方程就是dp[i][j] = dp[i][j-1]。 -
情况2:
s[i] != s[j]
此时必须将区间分成两部分分别打印,因为一次打印无法同时满足首尾不同。
dp[i][j] = min{dp[i][k] + dp[k+1][j]},其中k从i到j-1。
4. 最终答案
dp[0][n-1] 即为打印整个字符串的最少打印次数。
示例演算
以 s = "ABAB" 为例,n=4。
初始化:
dp[0][0]=1, dp[1][1]=1, dp[2][2]=1, dp[3][3]=1
len=2:
- [0,1]: s[0]='A', s[1]='B', 不同 => min{dp[0][0]+dp[1][1]} = 1+1=2
所以 dp[0][1]=2。 - [1,2]: s[1]='B', s[2]='A', 不同 => dp[1][2]=2。
- [2,3]: s[2]='A', s[3]='B', 不同 => dp[2][3]=2。
len=3:
- [0,2]: s[0]='A', s[2]='A', 相同 => dp[0][2] = dp[0][1] = 2。
验证:"ABA",先打全'A'(1次),再打中间'B'(2次),总共2次。 - [1,3]: s[1]='B', s[3]='B', 相同 => dp[1][3] = dp[1][2] = 2。
验证:"BAB",先打全'B'(1次),再打中间'A'(2次),总共2次。
len=4:
- [0,3]: s[0]='A', s[3]='B', 不同 => 枚举 k:
k=0: dp[0][0]+dp[1][3]=1+2=3
k=1: dp[0][1]+dp[2][3]=2+2=4
k=2: dp[0][2]+dp[3][3]=2+1=3
最小值 = 3。
所以 dp[0][3]=3。
最终答案:3。
算法复杂度
- 时间复杂度:O(n³),因为需要枚举区间和分割点。
- 空间复杂度:O(n²),存储 dp 表。
核心要点总结
- 当首尾字符相同时,可以节省一次打印,因为可以在某次打印中覆盖到末尾。
- 当首尾不同时,必须分割区间分别处理。
- 这个DP方程与字符集大小无关,但两种颜色可以帮助我们更直观地理解“覆盖”的含义。
通过这个例题,你能够掌握区间DP在处理“覆盖类”打印问题中的基本思路,即通过首尾字符关系减少状态转移的分支。