奇怪的打印机问题(进阶版:每次只能打印一种颜色,但可任意覆盖相邻字符,且目标串为环形)
题目描述
有一台奇怪的打印机,它具有以下特点:
- 每次打印时,可以选择任意一个起始位置和任意一个结束位置(在环上连续),将这个区间内的所有字符用同一种颜色打印。
- 如果某个位置已经被打印过,再次打印时会覆盖之前的颜色。
- 打印机的目标是最终使整个环形字符串与给定的目标字符串完全相同。
- 每次打印的成本为 1,求打印出整个目标环形字符串所需的最少打印次数。
注意:字符串是环形的,即第一个字符和最后一个字符相邻。
例如,目标字符串为 "ababa",将其视为环形,则打印区间可以跨越首尾(如从位置4到位置1的区间包含字符 'a','b','a')。
解题思路循序渐进
步骤1:将环形问题转化为线性问题
由于字符串是环形的,为了处理方便,我们常用的技巧是:将原字符串复制一份接在末尾,这样得到长度为 2n 的线性字符串。
例如,目标字符串 s = "ababa",n=5,构造 t = s + s = "ababaababa"。
现在,问题转化为:打印长度为 2n 的线性字符串 t 的前 n 个字符与目标匹配(后 n 个字符是副本,无需完全匹配,但打印操作可以覆盖到副本部分)。
但因为我们要求的是打印整个环,而环上每个位置都必须正确,所以我们可以枚举环的起点,对每个起点开始的长为 n 的子问题进行线性DP求解,最后取最小值。
步骤2:定义状态
对于线性版本的问题(给定长度为 m 的字符串 s,求打印出 s 的最少次数),定义状态:
dp[i][j] 表示打印出子串 s[i..j](闭区间)所需的最少打印次数。
目标是求 dp[0][n-1]。
步骤3:状态转移分析
考虑最后一次打印操作覆盖的区间。
由于打印机每次打印一个连续区间,并且将该区间全部涂成同一种颜色,该颜色必须与目标字符串在该区间内的颜色一致。
因此,如果 s[i] == s[j],那么可以在某一次打印中同时覆盖 i 和 j,且颜色为 s[i]。
但注意:如果 s[i] == s[j],我们可以将问题分解为:先打印出 s[i..j-1] 或 s[i+1..j],因为最后一次打印可以覆盖到 j 且颜色与 i 处相同,所以 dp[i][j] = dp[i][j-1] 或 dp[i+1][j]。
更一般地,对于任意区间 [i,j],我们可以将区间分成两部分 [i,k] 和 [k+1,j],分别打印,然后合并。但这样可能会多算次数,因为如果 s[i] 的颜色与后面某处相同,可能可以一次打印覆盖。
实际上,最优策略是:
- 首先打印 s[i] 的颜色覆盖某个区间(可能到某个位置 k)。
- 然后打印中间其他部分。
但更高效的转移是:
如果 s[i] == s[k](i < k <= j),那么我们可以先打印出 s[i..k](颜色为 s[i]),然后再打印其他部分。但这样会重复计算 s[i] 的打印。
标准的状态转移方程为:
dp[i][j] = dp[i][j-1] (如果 s[i] == s[j])
否则,dp[i][j] = min{ dp[i][k] + dp[k+1][j] },其中 i <= k < j。
解释:
- 当 s[i] == s[j] 时,我们可以在打印 s[i] 的那次操作中顺带覆盖到 j(因为颜色相同),所以 dp[i][j] 至少不会比 dp[i][j-1] 差(可能更优,但不会更差)。实际上可以证明 dp[i][j] = dp[i][j-1]。
- 当 s[i] != s[j] 时,我们需要将区间分成两部分分别打印,因为首尾颜色不同,不可能一次打印同时覆盖 i 和 j。
步骤4:初始化
对于长度为 1 的区间,dp[i][i] = 1,因为只需要一次打印即可完成一个字符。
步骤5:环形处理
对于环形问题,我们枚举起点 start = 0,1,...,n-1,取线性字符串 t[start : start+n] 作为当前线性问题,计算其 dp[0][n-1],然后取所有 start 对应的最小值。
步骤6:算法步骤
- 输入字符串 s,长度为 n。
- 如果 n == 0,返回 0。
- 将 s 复制一份得到 t = s + s。
- 定义函数 linear_min_print(s_linear) 计算线性字符串的最少打印次数。
- 实现 linear_min_print:
- m = len(s_linear)
- dp 为 m×m 的二维数组,初始化为 0。
- 对每个 i,dp[i][i] = 1。
- 按长度 len 从 2 到 m 递增:
- 对每个 i,j = i+len-1,如果 j >= m 则跳出。
- 如果 s_linear[i] == s_linear[j]:
dp[i][j] = dp[i][j-1] # 因为可以一次打印覆盖 i 到 j(颜色相同)
else:
dp[i][j] = min( dp[i][k] + dp[k+1][j] for k in range(i, j) )
- 对每个 start in 0..n-1:
cur = t[start:start+n]
ans = min(ans, linear_min_print(cur)) - 返回 ans。
举例验证
例:s = "ababa",n=5。
先看线性版本(非环形):"ababa"。
- dp[0][0]=1, dp[1][1]=1, dp[2][2]=1, dp[3][3]=1, dp[4][4]=1。
- len=2:
- [0,1]: 'a'!='b' -> min(dp[0][0]+dp[1][1]=2) = 2。
- [1,2]: 'a'== 'a' -> dp[1][1]=1。
- [2,3]: 'a'!='b' -> 2。
- [3,4]: 'a'== 'a' -> dp[3][3]=1。
- len=3:
- [0,2]: s[0]=='a', s[2]=='a' -> dp[0][1]=2? 等一等,这里 s[0]==s[2],所以 dp[0][2] = dp[0][1]? 但 dp[0][1]=2,而实际上"aba"可以两次打印:先打印整个区间为'a',再在中间打'b',所以是2,正确。
计算:若按方程,s[0]==s[2],则 dp[0][2]=dp[0][1]=2。 - [1,3]: s[1]=='b', s[3]=='b' -> dp[1][3]=dp[1][2]=? dp[1][2] 中 s[1]==s[2]? 不,'b'!='a',所以 dp[1][2] 是 min(dp[1][1]+dp[2][2]=2) =2。所以 dp[1][3]=2。
- [2,4]: s[2]=='a', s[4]=='a' -> dp[2][4]=dp[2][3]=2。
- [0,2]: s[0]=='a', s[2]=='a' -> dp[0][1]=2? 等一等,这里 s[0]==s[2],所以 dp[0][2] = dp[0][1]? 但 dp[0][1]=2,而实际上"aba"可以两次打印:先打印整个区间为'a',再在中间打'b',所以是2,正确。
- len=4:
- [0,3]: s[0]=='a', s[3]=='b' 不同 -> 分割:
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
min=3。 - [1,4]: s[1]=='b', s[4]=='a' 不同 -> 分割:
k=1: 2+2=4, k=2: 2+2=4, k=3: 2+1=3 -> min=3。
- [0,3]: s[0]=='a', s[3]=='b' 不同 -> 分割:
- len=5: [0,4]: s[0]=='a', s[4]=='a' 相同 -> dp[0][4]=dp[0][3]=3。
所以线性"ababa"需要 3 次。
但环形可能更优:
例如,从 start=1 得到 "babaa"。
计算线性"babaa":
- len2: [0,1]: 'b'!='a'=2, [1,2]:'a'!='b'=2, [2,3]:'a'=='a'=1, [3,4]:'a'=='a'=1。
- len3: [0,2]: 'b'!='b'? 不,s[0]=='b', s[2]=='b' -> 相同 -> dp[0][2]=dp[0][1]=2。
[1,3]: 'a'!='a'? 相同 -> dp[1][3]=dp[1][2]=2。
[2,4]: 'b'!='a' -> 分割:2+1=3, 1+1=2, 1+1=2 -> min=2。 - len4: [0,3]: 'b'!='a' -> 分割:k=0:1+2=3, k=1:2+1=3, k=2:2+1=3 -> 3。
[1,4]: 'a'=='a' -> dp[1][4]=dp[1][3]=2。 - len5: [0,4]: 'b'!='a' -> 分割:k=0:1+2=3, k=1:2+2=4, k=2:2+1=3, k=3:3+1=4 -> 3。
所以"babaa"也需要 3 次。
尝试 start=2 得到 "abaab",计算得 3 次。
实际上,这个例子环形并不比线性更优,答案仍是 3。
步骤7:复杂度
线性DP的复杂度为 O(n³),加上枚举起点 O(n),总复杂度 O(n⁴),在 n 较小时可接受。但可以通过优化状态转移,利用环形DP的技巧(如破环成链后直接计算长度为 n 的区间在 2n 串上的DP)将复杂度降为 O(n³)。具体为:
直接构建 t = s+s,长度 2n,然后计算 dp[i][j] for 0<=i<j<2n,然后答案 = min{ dp[i][i+n-1] for i=0..n-1 }。
最终答案
算法返回 min_{i=0..n-1} dp[i][i+n-1],其中 dp 基于字符串 t = s+s 计算。
通过上述步骤,我们解决了环形版本的奇怪打印机问题。