合并相邻字符形成目标串的最小操作次数问题(字符插入、删除、替换的加权版本)
字数 4387 2025-12-08 08:01:15

合并相邻字符形成目标串的最小操作次数问题(字符插入、删除、替换的加权版本)

问题描述
给定两个字符串 sourcetarget,以及三种操作的成本:

  1. 插入一个字符,成本为 insertCost
  2. 删除一个字符,成本为 deleteCost
  3. 替换一个字符(将 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 中的某个连续段(由相同字符组成),要么在中间插入新的段。这类似于“带权编辑距离”,但操作单位是连续相同字符的段,而不是单个字符。

我们可以先对 sourcetarget 进行分段,将连续相同字符压缩成“段”,每段记录字符和长度。例如,source = "aabb" 分段为 [('a',2), ('b',2)]。然后问题转化为:将 source 的段序列转换为 target 的段序列,每次操作可以删除一段、插入一段、或替换一段(替换时字符可以不同,但成本固定为 replaceCost)。但这里替换操作要求替换后的段字符全部相同,这与分段表示一致。

更精确的 DP 定义
dp[i][j] 表示将 source 的前 i 段转换为 target 的前 j 段的最小成本。状态转移时,我们考虑最后一段的匹配方式:

  1. 删除 source 的最后一段,成本为 deleteCost,状态转移为 dp[i-1][j] + deleteCost
  2. 插入 target 的最后一段,成本为 insertCost,状态转移为 dp[i][j-1] + insertCost
  3. 替换:将 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] 转换为空串的最小成本。然后,对于每个区间,我们有两种选择:

  1. 直接删除整个区间,成本为 deleteCost
  2. 将区间分成两部分处理,成本为 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),太大。我们需要优化。

注意到操作的性质,我们可以将问题分解为:首先将 sourcetarget 分段,得到段序列 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] 的字符可以不同,但成本固定。

这个转移方程就是经典的最小编辑距离,但这里的“段”是连续相同字符的块。这样,我们成功将区间操作转化为段操作,因为每个操作都是针对整个段进行的。

算法步骤

  1. sourcetarget 分别分段,得到两个列表 seg_sseg_t,每个元素是 (char, length)
  2. n = len(seg_s), m = len(seg_t)。定义二维数组 dp 大小为 (n+1) x (m+1)dp[i][j] 表示将 seg_s 的前 i 段转换为 seg_t 的前 j 段的最小成本。
  3. 初始化:
    • dp[0][0] = 0
    • dp[i][0] = i * deleteCost(删除前 i 段)
    • dp[0][j] = j * insertCost(插入前 j 段)
  4. 状态转移:
    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)
    
  5. 最终答案为 dp[n][m]

时间复杂度:O(nm),其中 n 和 m 分别是 sourcetarget 的段数,在最坏情况下(字符串没有连续相同字符)等于原串长度。


示例说明
假设 source = "aaabbb", target = "ab", insertCost = 1, deleteCost = 1, replaceCost = 2

  1. 分段:
    • seg_s: [('a',3), ('b',3)]
    • seg_t: [('a',1), ('b',1)]
  2. 初始化 dp
    • dp[0][0]=0, dp[1][0]=1, dp[2][0]=2, dp[0][1]=1, dp[0][2]=2
  3. 计算:
    • dp[1][1]: min(dp[0][1]+1=2, dp[1][0]+1=2, dp[0][0]+2=2) = 2
    • dp[1][2]: min(dp[0][2]+1=3, dp[1][1]+1=3, dp[0][1]+2=3) = 3
    • dp[2][1]: min(dp[1][1]+1=3, dp[2][0]+1=3, dp[1][0]+2=3) = 3
    • dp[2][2]: min(dp[1][2]+1=4, dp[2][1]+1=4, dp[1][1]+2=4) = 4
  4. 答案为 4。

解释:一种最优方案是:将 source 的第一段('a',3) 替换为 ('a',1),成本2;将第二段('b',3) 替换为 ('b',1),成本2;总成本4。


总结
本题通过将字符串按连续相同字符分段,将区间操作问题转化为段序列的最小编辑距离问题,从而可以用二维DP高效求解。核心在于理解“操作针对整个区间”这一限制等价于操作的单位是连续相同字符的段。

合并相邻字符形成目标串的最小操作次数问题(字符插入、删除、替换的加权版本) 问题描述 给定两个字符串 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] = 0 dp[i][0] = i * deleteCost (删除前 i 段) dp[0][j] = j * insertCost (插入前 j 段) 状态转移: 最终答案为 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) = 2 dp[1][2] : min(dp[ 0][ 2]+1=3, dp[ 1][ 1]+1=3, dp[ 0][ 1 ]+2=3) = 3 dp[2][1] : min(dp[ 1][ 1]+1=3, dp[ 2][ 0]+1=3, dp[ 1][ 0 ]+2=3) = 3 dp[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高效求解。核心在于理解“操作针对整个区间”这一限制等价于操作的单位是连续相同字符的段。