最长公共子序列的变种:带字符插入/删除/替换代价的最短编辑距离(进阶版:带位置相关的操作代价)
题目描述
给定两个字符串 word1 和 word2,以及三种操作的代价函数:
- 插入操作:在第
i个字符后插入一个字符,代价为ins_cost(i, ch)。 - 删除操作:删除
word1的第i个字符,代价为del_cost(i, ch)。 - 替换操作:将
word1的第i个字符替换为word2的第j个字符,代价为rep_cost(i, j, ch1, ch2)。
注意:代价函数可能依赖于位置和字符本身。
目标:计算将 word1 转换为 word2 的最小总代价。
示例
假设:
- word1 = "abc", word2 = "adc"
- 插入代价:在任意位置插入任意字符,代价为 1
- 删除代价:删除任意位置字符,代价为 1
- 替换代价:若字符相同则为 0,否则为 2
则最小代价为 0(因为只需替换 'b' 为 'd',但此处 'b' != 'd',所以替换代价为 2,但也可以删除 'b' 再插入 'd',总代价 1+1=2)。
最终结果为 2。
解题思路
这是编辑距离问题的泛化版本,核心仍然是动态规划。但代价函数与位置相关,因此状态设计需包含当前处理到的位置。
步骤 1:状态定义
设 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符的最小代价。
这里 i 从 0 到 len(word1),j 从 0 到 len(word2)。
注意:word1 的前 i 个字符下标是 0 到 i-1。
步骤 2:初始状态
dp[0][0] = 0:两个空字符串无需操作。dp[i][0]:将word1的前i个字符全部删除,代价为删除这些字符的代价之和。
即:dp[i][0] = dp[i-1][0] + del_cost(i-1, word1[i-1])。dp[0][j]:从空串构造word2的前j个字符,全部插入,代价为插入这些字符的代价之和。
即:dp[0][j] = dp[0][j-1] + ins_cost(j-1, word2[j-1])(注意:插入位置可能是前一个字符之后,此处假设ins_cost(k, ch)表示在已构建的长度为 k 的字符串后插入 ch 的代价,但题目中ins_cost(i, ch)的 i 是原 word1 的位置索引,这里需根据题意调整,常见简化为代价与位置无关,我们先按与位置相关但只依赖原串位置来推导)。
步骤 3:状态转移方程
考虑从 dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1] 转移到 dp[i][j]:
- 替换或保持:
将word1[i-1]替换为word2[j-1]。
若word1[i-1] == word2[j-1],则替换代价可能是 0 或由rep_cost决定。
总代价 =dp[i-1][j-1] + rep_cost(i-1, j-1, word1[i-1], word2[j-1])。 - 删除:
删除word1[i-1],然后处理word1前 i-1 个字符到word2前 j 个字符。
总代价 =dp[i-1][j] + del_cost(i-1, word1[i-1])。 - 插入:
在word1的前 i 个字符后(即当前已处理到 i 的位置)插入word2[j-1],然后处理word1前 i 个字符到word2前 j-1 个字符。
总代价 =dp[i][j-1] + ins_cost(i-1, word2[j-1])(这里ins_cost的第一个参数表示在 word1 的第 i-1 个字符后插入,如果 i=0 则表示在开头插入,需根据题意定义)。
取三者最小值:
dp[i][j] = min( replace_cost, delete_cost, insert_cost )
步骤 4:边界与细节
- 如果
ins_cost、del_cost、rep_cost是常数函数,则退化为标准编辑距离。 - 如果代价与位置相关,在递推时需传入当前下标。
- 注意索引:
word1的第 i 个字符下标是 i-1。 - 如果
ins_cost的第一个参数表示“在原串的第 k 个字符后插入”,那么在dp[i][j-1]转移到dp[i][j]时,我们已经用掉了 word1 的前 i 个字符,下一个插入位置可以认为是第 i 个字符后(即下标 i-1 之后),所以用ins_cost(i-1, word2[j-1])是合理的。 - 如果 i=0,表示在原串开头插入,此时
ins_cost可能需要特殊定义(比如ins_cost(-1, ch)表示在开头插入)。在代码中可特殊处理。
步骤 5:举例演算
假设 word1 = "ab", word2 = "bc",代价函数简化为常数:ins=1, del=1, rep=2。
dp 表大小 3×3。
初始化:
- dp[0][0] = 0
- dp[1][0] = dp[0][0] + del('a') = 0+1=1
- dp[2][0] = dp[1][0] + del('b') = 1+1=2
- dp[0][1] = dp[0][0] + ins('b') = 0+1=1
- dp[0][2] = dp[0][1] + ins('c') = 1+1=2
计算 dp[1][1]("a"→"b"):
- 替换:dp[0][0] + rep('a','b') = 0+2=2
- 删除:dp[0][1] + del('a') = 1+1=2
- 插入:dp[1][0] + ins('b') = 1+1=2
dp[1][1] = 2
计算 dp[1][2]("a"→"bc"):
- 替换:dp[0][1] + rep('a','b') = 1+2=3
- 删除:dp[0][2] + del('a') = 2+1=3
- 插入:dp[1][1] + ins('c') = 2+1=3
dp[1][2] = 3
计算 dp[2][1]("ab"→"b"):
- 替换:dp[1][0] + rep('b','b') = 1+0=1
- 删除:dp[1][1] + del('b') = 2+1=3
- 插入:dp[2][0] + ins('b') = 2+1=3
dp[2][1] = 1
计算 dp[2][2]("ab"→"bc"):
- 替换:dp[1][1] + rep('b','c') = 2+2=4
- 删除:dp[1][2] + del('b') = 3+1=4
- 插入:dp[2][1] + ins('c') = 1+1=2
dp[2][2] = 2
最小代价为 2。
步骤 6:算法复杂度
时间复杂度 O(mn),空间复杂度 O(mn)(可优化到 O(min(m, n)))。
步骤 7:代码框架(Python)
def min_edit_distance_cost(word1, word2, ins_cost, del_cost, rep_cost):
m, n = len(word1), len(word2)
dp = [[0]*(n+1) for _ in range(m+1)]
# 初始化删除所有字符的代价
for i in range(1, m+1):
dp[i][0] = dp[i-1][0] + del_cost(i-1, word1[i-1])
# 初始化插入所有字符的代价
for j in range(1, n+1):
# 在开头插入的特殊处理,假设 ins_cost(-1, ch) 表示在开头插入
dp[0][j] = dp[0][j-1] + ins_cost(j-2 if j>1 else -1, word2[j-1])
for i in range(1, m+1):
for j in range(1, n+1):
replace = dp[i-1][j-1] + rep_cost(i-1, j-1, word1[i-1], word2[j-1])
delete = dp[i-1][j] + del_cost(i-1, word1[i-1])
# 插入 word2[j-1] 在 word1 的第 i-1 字符后
insert = dp[i][j-1] + ins_cost(i-1, word2[j-1])
dp[i][j] = min(replace, delete, insert)
return dp[m][n]
总结
这个问题是编辑距离的泛化,关键在于代价函数与位置相关时,状态转移时需传入正确的索引。实际应用中,代价函数可能以查表形式给出,只需在计算时获取对应代价即可。