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

本文涉及的产品
资源编排,不限时长
简介: 动态规划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] ]
  • 这表示我们不对这对字符进行任何编辑,直接继承左上角单元格的编辑操作数。
  1. 字符不匹配(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) 分别是两个字符串的长度。

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

初始表格结构如下:

R O S
0 1 2 3
H 1
O 2
R 3
S 4
E 5
  • 第一行第一列 的填充是基于单字符插入和删除操作的累积成本。例如,第一行第二格的 1 表示将空串变为 “R” 需要一次插入操作,依此类推。
填充过程

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

  1. H vs R:
  • 不匹配。取 min(左方, 上方, 左上方+1)min(1, 1, 1) = 1 (dp[1][1] 表示从"H"到"R"的最小编辑距离)
  1. H vs O:
  • 不匹配。min(1, 2, 2) = 2 (由 “H” 到 “RO” 或由 “HO” 到 “R”)
  1. H vs S:
  • 不匹配。min(2, 3, 3) = 3 (由 “H” 到 “ROS” 或由 “HS” 到 “RO”)
  1. O vs R:
  • 不匹配。min(2, 1, 2) = 2 (由 “O” 到 “R” 或由 “HO” 到 “RO”)
  1. O vs O:
  • 匹配。dp[1][1] + 0 = 1 (无操作, 因为 “O” 匹配 “O”)
  1. O vs S:
  • 不匹配。min(3, 2, 2) = 2 (由 “O” 到 “ROS” 或由 “HO” 到 “OS”)
  1. R vs R:
  • 匹配。dp[2][1] + 0 = 2 (无操作, 因为 “R” 匹配 “R”)
  1. R vs O:
  • 不匹配。min(2, 3, 3) = 3 (由 “HR” 到 “RO” 或由 “HOR” 到 “R”)
  1. R vs S:
  • 不匹配。min(3, 4, 3) = 3 (由 “HR” 到 “ROS” 或由 “HOR” 到 “OS”)
  1. Final Row (E):
  • 类似上述步骤,我们继续用 s1 的 “E” 与 s2 的每个字符进行比较填表。

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

R O S
0 1 2 3
H 1 1 2 3
O 2 2 1 2
R 3 2 2 2
S 4 3 3 2
E 5 4 4 3
逐步分析与填充
  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
  1. 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
完整填充后的表格
R O S
0 1 2 3
H 1 1 2 3
O 2 2 1 2
R 3 2 2 2
S 4 3 3 2
E 5 4 4 3

在最右下角的单元格 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]。所以我们进行替换操作。
  1. 从 ‘s’ 到 ‘s’
  • 位置 (4, 2): 此时字符 ‘s’ 和 ‘s’ 匹配。
  • 由于字符匹配,我们直接沿对角线向上移动到 dp[3][1],无需额外操作。
  1. 从 ‘r’ 到 ‘o’
  • 位置 (3, 1): 此时字符 ‘r’ 和 ‘o’ 不匹配。
  • 可以通过将 ‘r’ 替换为 ‘o’ 来减少一个不匹配,即 dp[2][0]。所以我们进行替换操作。
  1. 从 ‘o’ 到 ‘’ (空)
  • 位置 (2, 0): 此时我们需要将 ‘o’ 删除,因为s2已无更多字符。
  • 沿着第一列向上,每次删除 s1 中的字符,直到 dp[1][0]
  1. 从 ‘h’ 到 ‘’ (空)
  • 位置 (1, 0): 最后,删除 ‘h’,完成所有转换。
结果

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

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

5.总结

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

欢迎关注微信公众号 数据分析螺丝钉

相关实践学习
使用ROS创建VPC和VSwitch
本场景主要介绍如何利用阿里云资源编排服务,定义资源编排模板,实现自动化创建阿里云专有网络和交换机。
阿里云资源编排ROS使用教程
资源编排(Resource Orchestration)是一种简单易用的云计算资源管理和自动化运维服务。用户通过模板描述多个云计算资源的依赖关系、配置等,并自动完成所有资源的创建和配置,以达到自动化部署、运维等目的。编排模板同时也是一种标准化的资源和应用交付方式,并且可以随时编辑修改,使基础设施即代码(Infrastructure as Code)成为可能。 产品详情:https://www.aliyun.com/product/ros/
相关文章
|
4月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
72 8
|
9天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
26 2
|
26天前
|
存储 算法 Python
【10月更文挑战第16天】「Mac上学Python 27」小学奥数篇13 - 动态规划入门
本篇将通过 Python 和 Cangjie 双语介绍动态规划的基本概念,并解决一个经典问题:斐波那契数列。学生将学习如何使用动态规划优化递归计算,并掌握编程中的重要算法思想。
93 3
|
4月前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
121 2
|
4月前
|
算法 程序员 Python
算法小白到大神的蜕变之路:Python分治法、贪心、动态规划,一步步带你走向算法巅峰!
【7月更文挑战第9天】探索算法之旅,以Python解锁编程高手之路。分治法如二分查找,将复杂问题拆解;贪心算法解决活动选择,每次选取局部最优;动态规划求斐波那契数列,避免重复计算,实现全局最优。每一步学习,都是编程能力的升华,助你应对复杂挑战,迈向算法大师!
47 1
|
4天前
|
机器学习/深度学习 人工智能 TensorFlow
人工智能浪潮下的自我修养:从Python编程入门到深度学习实践
【10月更文挑战第39天】本文旨在为初学者提供一条清晰的道路,从Python基础语法的掌握到深度学习领域的探索。我们将通过简明扼要的语言和实际代码示例,引导读者逐步构建起对人工智能技术的理解和应用能力。文章不仅涵盖Python编程的基础,还将深入探讨深度学习的核心概念、工具和实战技巧,帮助读者在AI的浪潮中找到自己的位置。
|
4天前
|
机器学习/深度学习 数据挖掘 Python
Python编程入门——从零开始构建你的第一个程序
【10月更文挑战第39天】本文将带你走进Python的世界,通过简单易懂的语言和实际的代码示例,让你快速掌握Python的基础语法。无论你是编程新手还是想学习新语言的老手,这篇文章都能为你提供有价值的信息。我们将从变量、数据类型、控制结构等基本概念入手,逐步过渡到函数、模块等高级特性,最后通过一个综合示例来巩固所学知识。让我们一起开启Python编程之旅吧!
|
4天前
|
存储 Python
Python编程入门:打造你的第一个程序
【10月更文挑战第39天】在数字时代的浪潮中,掌握编程技能如同掌握了一门新时代的语言。本文将引导你步入Python编程的奇妙世界,从零基础出发,一步步构建你的第一个程序。我们将探索编程的基本概念,通过简单示例理解变量、数据类型和控制结构,最终实现一个简单的猜数字游戏。这不仅是一段代码的旅程,更是逻辑思维和问题解决能力的锻炼之旅。准备好了吗?让我们开始吧!
|
6天前
|
设计模式 算法 搜索推荐
Python编程中的设计模式:优雅解决复杂问题的钥匙####
本文将探讨Python编程中几种核心设计模式的应用实例与优势,不涉及具体代码示例,而是聚焦于每种模式背后的设计理念、适用场景及其如何促进代码的可维护性和扩展性。通过理解这些设计模式,开发者可以更加高效地构建软件系统,实现代码复用,提升项目质量。 ####