动态规划Dynamic programming详解-编辑距离问题【python】

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
LeetCode解锁1000题: 打怪升级之旅
python数据分析可视化:企业实战案例
备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级

动态规划是解决各种优化问题的强大工具,特别是在问题可以分解为重叠的子问题时。接下来,我将介绍一个流行的动态规划案例——编辑距离问题(又称Levenshtein距离)

1. 问题介绍和应用场景

编辑距离问题是一个经典的问题,用于量化两个字符串之间的差异,即将一个字符串转换成另一个字符串所需的最小编辑操作次数,包括插入、删除和替换字符。编辑距离广泛应用于自然语言处理、文本相似度检测、拼写检查、生物信息学等领域。

2. 问题定义和示例

定义:给定两个字符串 s1s2,计算将 s1 转换成 s2 所需的最少操作数。

示例

  • s1 = "horse"
  • s2 = "ros"

最少操作数为 3(删除 ‘h’,替换 ‘o’ 为 ‘r’,删除 ‘e’)。
在编辑距离问题中,状态转移方程是用来描述如何从较小问题的解构建较大问题的解的数学表达式。它核心地指示了在动态规划表中如何更新每个单元格的值。以下是详细的推导和解释。

3.状态转移方程

状态定义

dp[i][j] 为从字符串 s1 的前 i 个字符转换到 s2 的前 j 个字符所需的最小编辑操作数。这里,s1[0..i-1]s2[0..j-1] 分别表示字符串 s1s2 的前 ij 个字符。

状态转移方程的推导

考虑以下几种情况:

  1. 字符匹配(s1[i-1] == s2[j-1]:

    • 如果当前两个字符相同,那么这一对字符不需要任何编辑操作。因此,当前问题的解可以直接继承前一个问题的解,即:
      [ dp[i][j] = dp[i-1][j-1] ]
    • 这表示我们不对这对字符进行任何编辑,直接继承左上角单元格的编辑操作数。
  2. 字符不匹配(s1[i-1] != s2[j-1]:

    • 如果当前两个字符不相同,我们有三种策略来使得 s1[0..i-1] 转换为 s2[0..j-1]
      • 删除操作:从 s1 中删除一个字符后,尝试匹配 s1[0..i-2]s2[0..j-1],操作数为 dp[i-1][j] + 1
      • 插入操作:向 s1 中插入一个与 s2[j-1] 相同的字符,然后尝试匹配 s1[0..i-1]s2[0..j-2],操作数为 dp[i][j-1] + 1
      • 替换操作:将 s1[i-1] 替换为与 s2[j-1] 相同的字符,然后匹配 s1[0..i-2]s2[0..j-2],操作数为 dp[i-1][j-1] + 1
    • 综上,我们选择上述三种策略中的最小值:
      [ dp[i][j] = 1 + \min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) ]

完整的状态转移方程

综合以上情况,编辑距离问题的状态转移方程可以表达为:

在这里插入图片描述

边界条件

  • 对于 i = 0 (即 s1 为空字符串时),将 s2 的前 j 个字符全部插入到 s1 是唯一的选项,因此 dp[0][j] = j
  • 对于 j = 0 (即 s2 为空字符串时),将 s1 的前 i 个字符全部删除是唯一的选项,因此 dp[i][0] = i

通过上述状态转移方程,我们可以系统地填充整个动态规划表,并最终解决编辑距离问题。这种方法不仅确保了解的正确性,而且通过避免冗余计算,提高了

4. 算法实现

def edit_distance(s1: str, s2: str) -> int:
    """
    计算两个字符串s1和s2之间的最小编辑距离。
    最小编辑距离是将s1转换成s2所需的最少单字符编辑操作次数(插入、删除、替换)。

    参数:
    s1 (str): 源字符串。
    s2 (str): 目标字符串。

    返回:
    int: s1转换成s2的最小编辑距离。
    """
    m, n = len(s1), len(s2)
    # 创建一个二维数组dp,大小为(m+1)x(n+1)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    # 初始化dp数组的第一行和第一列
    for i in range(m + 1):
        dp[i][0] = i  # 将s1的前i个字符删除
    for j in range(n + 1):
        dp[0][j] = j  # 将s2的前j个字符插入到s1中

    # 填充dp数组的其余部分
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]  # 字符相同,无需编辑
            else:
                dp[i][j] = 1 + min(dp[i - 1][j],    # 删除操作
                                   dp[i][j - 1],    # 插入操作
                                   dp[i - 1][j - 1])  # 替换操作

    return dp[m][n]  # 返回将整个s1转换成s2的最小编辑距离

# 示例使用
s1 = "horse"
s2 = "ros"
print("最小编辑距离:", edit_distance(s1, s2))

复杂度分析

  • 时间复杂度:O(mn),因为需要填充一个 m x n 的矩阵。
  • 空间复杂度:O(mn),可以优化到 O(min(m, n)) 通过只保留当前和前一行的状态。
    为了清晰地解释编辑距离问题的动态规划解法,并辅以图解,我们可以使用一个示例和配套的表格。我们将采用简单的字符串 s1 = "horse"s2 = "ros" 来阐述过程。

算法图解

动态规划表初始化

创建一个表格,其中行表示字符串 s1 的前 i 个字符,列表示字符串 s2 的前 j 个字符。我们使用一个 (len(s1)+1) x (len(s2)+1) 的表格,len(s1)len(s2) 分别是两个字符串的长度。

初始化表格(第一行和第一列特殊处理)

初始表格结构如下:

ROS
0123
H1
O2
R3
S4
E5
  • 第一行第一列 的填充是基于单字符插入和删除操作的累积成本。例如,第一行第二格的 1 表示将空串变为 “R” 需要一次插入操作,依此类推。
填充过程

遍历 s1 的每个字符(行),与 s2 的每个字符(列)进行比较:

  1. H vs R:

    • 不匹配。取 min(左方, 上方, 左上方+1)min(1, 1, 1) = 1 (dp[1][1] 表示从"H"到"R"的最小编辑距离)
  2. H vs O:

    • 不匹配。min(1, 2, 2) = 2 (由 “H” 到 “RO” 或由 “HO” 到 “R”)
  3. H vs S:

    • 不匹配。min(2, 3, 3) = 3 (由 “H” 到 “ROS” 或由 “HS” 到 “RO”)
  4. O vs R:

    • 不匹配。min(2, 1, 2) = 2 (由 “O” 到 “R” 或由 “HO” 到 “RO”)
  5. O vs O:

    • 匹配。dp[1][1] + 0 = 1 (无操作, 因为 “O” 匹配 “O”)
  6. O vs S:

    • 不匹配。min(3, 2, 2) = 2 (由 “O” 到 “ROS” 或由 “HO” 到 “OS”)
  7. R vs R:

    • 匹配。dp[2][1] + 0 = 2 (无操作, 因为 “R” 匹配 “R”)
  8. R vs O:

    • 不匹配。min(2, 3, 3) = 3 (由 “HR” 到 “RO” 或由 “HOR” 到 “R”)
  9. R vs S:

    • 不匹配。min(3, 4, 3) = 3 (由 “HR” 到 “ROS” 或由 “HOR” 到 “OS”)
  10. Final Row (E):

    • 类似上述步骤,我们继续用 s1 的 “E” 与 s2 的每个字符进行比较填表。

继续填充表格的详细步骤:

ROS
0123
H1123
O2212
R3222
S4332
E5443
逐步分析与填充
  1. S vs R, O, S:

    • S vs R: 不匹配。min(左方, 上方, 左上方+1)min(3, 2, 3) = 2
    • S vs O: 不匹配。min(左方, 上方, 左上方+1)min(2, 3, 3) = 2
    • S vs S: 匹配。dp[4][3]dp[3][2] + 0 = 2
  2. E vs R, O, S:

    • E vs R: 不匹配。min(左方, 上方, 左上方+1)min(4, 3, 3) = 3
    • E vs O: 不匹配。min(左方, 上方, 左上方+1)min(3, 4, 4) = 3
    • E vs S: 不匹配。min(左方, 上方, 左上方+1)min(2, 3, 4) = 2
完整填充后的表格
ROS
0123
H1123
O2212
R3222
S4332
E5443

在最右下角的单元格 dp[5][3],我们得到 s1 = "horse"s2 = "ros" 的最小编辑距离为 3。这表明我们需要进行三次编辑操作(删除两个字符 ‘h’ 和 ‘e’,并将一个 ‘r’ 替换为 ‘o’)来将 “horse”
回溯过程转换成 “ros”。以下是更加具体的步骤和解释:

回溯过程

要详细了解如何从 “horse” 变成 “ros”,我们可以从填充完成的动态规划表格开始回溯。从表格的右下角 (dp[5][3]) 开始,每一步都选择了一个操作,直到我们回到表格的左上角 (dp[0][0])。

从表格回溯操作:

  1. 从 ‘e’ 到 ‘s’

    • 位置 (5, 3): 此时字符 ‘e’ (s1的第5个字符) 和 ‘s’ (s2的第3个字符) 不匹配。
    • 可以通过将 ‘e’ 替换为 ‘s’ 来减少一个不匹配,即 dp[4][2]。所以我们进行替换操作。
  2. 从 ‘s’ 到 ‘s’

    • 位置 (4, 2): 此时字符 ‘s’ 和 ‘s’ 匹配。
    • 由于字符匹配,我们直接沿对角线向上移动到 dp[3][1],无需额外操作。
  3. 从 ‘r’ 到 ‘o’

    • 位置 (3, 1): 此时字符 ‘r’ 和 ‘o’ 不匹配。
    • 可以通过将 ‘r’ 替换为 ‘o’ 来减少一个不匹配,即 dp[2][0]。所以我们进行替换操作。
  4. 从 ‘o’ 到 ‘’ (空)

    • 位置 (2, 0): 此时我们需要将 ‘o’ 删除,因为s2已无更多字符。
    • 沿着第一列向上,每次删除 s1 中的字符,直到 dp[1][0]
  5. 从 ‘h’ 到 ‘’ (空)

    • 位置 (1, 0): 最后,删除 ‘h’,完成所有转换。
结果

通过以上回溯步骤,我们可以确定将 “horse” 转换为 “ros” 的最小编辑操作序列是:

  • 替换 ‘e’ 为 ‘s’
  • 替换 ‘r’ 为 ‘o’
  • 删除 ‘h’
  • 删除 ‘e’

5.总结

编辑距离问题的动态规划解法提供了一个有效的方式来计算两个字符串的相似度。该问题不仅在理论计算中有着重要的地位,而且在许多实际应用中都有广泛的用途,如拼写纠正、DNA序列分析等。通过这种方法,我们可以准确地评估和处理字符串数据,支持复杂的数据分析和决策制定。

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据分析螺丝钉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值