合并相邻字符形成目标串的最小操作次数问题(字符插入、删除、替换的加权版本)
问题描述
给定两个字符串 source 和 target,以及三种操作的成本:
- 插入一个字符,成本为
insertCost。 - 删除一个字符,成本为
deleteCost。 - 替换一个字符(将
source中的一个字符替换为另一个字符),成本为replaceCost。
你需要将 source 转换为 target,但操作有一个关键限制:所有操作只能应用于 source 的相邻字符区间上,并且每次操作会将该区间内的所有字符视为一个整体进行处理。更具体地说,每次操作可以选择 source 的一个连续子串(区间),然后执行以下三种操作之一:
- 删除该子串,成本为
deleteCost。 - 在该子串前或后插入一个由相同字符组成的子串(长度任意),成本为
insertCost。 - 将该子串整体替换为另一个由相同字符组成的子串(长度可以不同),成本为
replaceCost。
注意:这里的“替换”操作相当于先删除原子串,再插入新子串,但总成本固定为 replaceCost,且新子串的字符必须全部相同。
求将 source 转换为 target 的最小总成本。
解题思路
这是一个区间动态规划问题,因为操作针对的是连续区间,且我们需要考虑如何将整个字符串逐步转换。定义 dp[i][j] 为将 source 的子串 source[i:j](左闭右开,从 i 到 j-1)转换为 target 所需的最小成本。我们需要计算 dp[0][n],其中 n 是 source 的长度。但这里 target 是固定的,所以状态转移时需要同时考虑 target 的匹配情况。
实际上,这个问题可以转化为:我们每次可以对 source 的一个区间执行三种操作,使其逐步匹配 target 的某个区间。因此,更合适的状态定义是:dp[l][r] 表示将 source[l:r] 转换为 target 的对应子串(从某个位置开始,长度可能不同)的最小成本。但这样状态不完整,因为我们需要知道 target 的匹配位置。
关键点:由于操作是对整个区间进行,我们可以将问题重新解释为:将 source 分割成若干连续段,每段要么被删除,要么被替换为 target 中的某个连续段(由相同字符组成),要么在中间插入新的段。这类似于“带权编辑距离”,但操作单位是连续相同字符的段,而不是单个字符。
我们可以先对 source 和 target 进行分段,将连续相同字符压缩成“段”,每段记录字符和长度。例如,source = "aabb" 分段为 [('a',2), ('b',2)]。然后问题转化为:将 source 的段序列转换为 target 的段序列,每次操作可以删除一段、插入一段、或替换一段(替换时字符可以不同,但成本固定为 replaceCost)。但这里替换操作要求替换后的段字符全部相同,这与分段表示一致。
更精确的 DP 定义:
设 dp[i][j] 表示将 source 的前 i 段转换为 target 的前 j 段的最小成本。状态转移时,我们考虑最后一段的匹配方式:
- 删除
source的最后一段,成本为deleteCost,状态转移为dp[i-1][j] + deleteCost。 - 插入
target的最后一段,成本为insertCost,状态转移为dp[i][j-1] + insertCost。 - 替换:将
source的最后一段替换为target的最后一段,成本为replaceCost,但要求替换后字符一致,即source最后一段的字符与target最后一段的字符相同。状态转移为dp[i-1][j-1] + replaceCost。
但这里有一个问题:替换操作在题目中定义为“将源区间整体替换为另一个由相同字符组成的子串”,所以替换时字符可以改变,但必须全部相同,且成本固定为 replaceCost。所以不需要字符相同条件。那么转移3可以无条件进行。
然而,这样会丢失“区间操作”的本质:我们每次操作的是 source 的一个连续区间,可能对应多个段。因此,上述分段DP无法处理合并多个段为一个操作的情况。
正确定义:
我们需要回到区间DP,定义 dp[l][r] 为将 source[l:r] 转换为空字符串的最小成本,因为如果我们要匹配 target 的某个子串,我们可以先将其转换为空,再插入 target 的子串。但这样不够高效。更标准的做法是定义 dp[i][j] 为将 source 的前 i 个字符转换为 target 的前 j 个字符的最小成本,但操作限制为:每次操作必须针对 source 的一个连续区间,且该区间在操作后被整体视为一个块。
实际上,这个问题是经典“编辑距离”的变种,但操作单位是连续区间,而不是单个字符。经过分析,可以发现:由于插入、删除、替换操作都是针对整个区间,且区间内的字符在操作后必须一致,这等价于我们可以将 source 分成若干块,每块可以独立操作。但块之间是独立的,所以问题可以简化为:对 source 的每个连续相同字符的区间,我们可以选择删除、替换(成另一个字符的连续串)、或者在旁边插入新串。
最终解法:
我们可以用区间 DP,状态 dp[i][j] 表示将 source[i:j] 转换为空串的最小成本。然后,对于每个区间,我们有两种选择:
- 直接删除整个区间,成本为
deleteCost。 - 将区间分成两部分处理,成本为
dp[i][k] + dp[k][j]。
但这样没有考虑替换操作。替换操作相当于:将整个区间替换为另一个字符的串,但因为我们目标为空,替换没有意义。所以我们需要将 target 纳入状态。
正确状态设计:
定义 dp[i][j][t] 表示将 source[i:j] 转换为 target 的前 t 个字符的最小成本。但状态太大。我们需要更聪明的定义。
经过思考,发现这个问题实际上等价于“最小编辑距离”,但每次操作针对的是 source 的一个连续子串,且操作后该子串变为一个单一字符的重复串。那么我们可以将 source 的每个位置看作一个字符,但操作可以合并相邻相同字符。这提示我们可以用区间DP,状态 dp[l][r] 表示将 source[l:r] 转换为 target 的对应子串的最小成本,但“对应”需要知道 target 的范围。
标准解法:
我们定义 dp[i][j][x][y] 表示将 source[i:j] 转换为 target[x:y] 的最小成本。但这样状态是 O(n^2 m^2),太大。我们需要优化。
注意到操作的性质,我们可以将问题分解为:首先将 source 和 target 分段,得到段序列 A 和 B。然后定义 f[i][j] 表示将 A 的前 i 段转换为 B 的前 j 段的最小成本。转移如下:
- 删除 A 的第 i 段:
f[i-1][j] + deleteCost - 插入 B 的第 j 段:
f[i][j-1] + insertCost - 替换 A 的第 i 段为 B 的第 j 段:
f[i-1][j-1] + replaceCost,这里替换要求 A[i] 和 B[j] 的字符可以不同,但成本固定。
这个转移方程就是经典的最小编辑距离,但这里的“段”是连续相同字符的块。这样,我们成功将区间操作转化为段操作,因为每个操作都是针对整个段进行的。
算法步骤:
- 将
source和target分别分段,得到两个列表seg_s和seg_t,每个元素是(char, length)。 - 设
n = len(seg_s),m = len(seg_t)。定义二维数组dp大小为(n+1) x (m+1),dp[i][j]表示将seg_s的前 i 段转换为seg_t的前 j 段的最小成本。 - 初始化:
dp[0][0] = 0dp[i][0] = i * deleteCost(删除前 i 段)dp[0][j] = j * insertCost(插入前 j 段)
- 状态转移:
for i from 1 to n: for j from 1 to m: cost_delete = dp[i-1][j] + deleteCost cost_insert = dp[i][j-1] + insertCost cost_replace = dp[i-1][j-1] + replaceCost dp[i][j] = min(cost_delete, cost_insert, cost_replace) - 最终答案为
dp[n][m]。
时间复杂度:O(nm),其中 n 和 m 分别是 source 和 target 的段数,在最坏情况下(字符串没有连续相同字符)等于原串长度。
示例说明
假设 source = "aaabbb", target = "ab", insertCost = 1, deleteCost = 1, replaceCost = 2。
- 分段:
seg_s: [('a',3), ('b',3)]seg_t: [('a',1), ('b',1)]
- 初始化
dp:dp[0][0]=0,dp[1][0]=1,dp[2][0]=2,dp[0][1]=1,dp[0][2]=2
- 计算:
dp[1][1]: min(dp[0][1]+1=2, dp[1][0]+1=2, dp[0][0]+2=2) = 2dp[1][2]: min(dp[0][2]+1=3, dp[1][1]+1=3, dp[0][1]+2=3) = 3dp[2][1]: min(dp[1][1]+1=3, dp[2][0]+1=3, dp[1][0]+2=3) = 3dp[2][2]: min(dp[1][2]+1=4, dp[2][1]+1=4, dp[1][1]+2=4) = 4
- 答案为 4。
解释:一种最优方案是:将 source 的第一段('a',3) 替换为 ('a',1),成本2;将第二段('b',3) 替换为 ('b',1),成本2;总成本4。
总结
本题通过将字符串按连续相同字符分段,将区间操作问题转化为段序列的最小编辑距离问题,从而可以用二维DP高效求解。核心在于理解“操作针对整个区间”这一限制等价于操作的单位是连续相同字符的段。