动态规划之编辑距离

Leetcode 72 编辑距离

本文层层递进,一共使用了五种方法,用Python解决了编辑距离问题。其根本思想是使用动态规划算法,根据状态转移方程即可求解。

题目再现

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
在这里插入图片描述
这个问题从直观上给人是很难的,但是在实际应用中非常常见。如下面的例子:输入法自动更正,当输入是左边的单词时,输入法会自动猜测到kitchen,而不是与实际输入差别很大的sitting。因此这种方法也能够衡量两个序列的相似度。例如DNA 序列是由 A,G,C,T 组成的序列,可以类比成字符串。编辑距离可以衡量两个 DNA 序列的相似度,编辑距离越小,说明这两段 DNA 越相似。
在这里插入图片描述

一 基本思想

1) 问题定义:编辑距离问题
Minimum Edit Distance, MED
输入
• 长度为𝒏的字符串𝒔,长度为𝒎的字符串𝒕
输出
• 求出一组编辑操作𝑶 =< 𝒆𝟏, 𝒆𝟐, … 𝒆𝒅 >,令
𝐦𝐢𝐧 |𝑶| ——优化目标
𝒔. 𝒕. 字符串𝒔经过𝑶的操作后满足𝒔 = 𝒕 ——约束条件
2)编辑操作
使用三种操作,即删除、插入、替换使得字符串𝒔变成字符串𝒕。
在这里插入图片描述
在这里插入图片描述
3) 递推公式
下面直接给出这个问题的递推公式,可以看出这与最长公共子序列的递推公式有类似之处。
在这里插入图片描述
在这里插入图片描述

二 代码实现

1) 递归法
这个方法的计算过程是自顶向上的,涉及了递归调用。

class Solution1:  # 递归
    def minEdietDistince(self, s1, s2):
        def dp(i, j):
            # base case
            if i == -1: return j + 1
            if j == -1: return i + 1
            
            if s1[i] == s2[j]:
                return dp(i-1, j-1)  # 跳过
            else:
                return min(
                    dp(i, j-1) + 1,   # 插入
                    dp(i-1, j) + 1,   # 删除
                    dp(i-1, j-1) + 1  # 替换
                )
        return dp(len(s1)-1, len(s2)-1)
s1 = "intention"
s2 = "execution"
print(Solution1().minEdietDistince(s1, s2))

2) 动态规划优化——增加备忘录
在上面递归的基础上增加备忘录,可减少计算时间。

class Solution2:  # 在前面的基础上增加备忘录,时间复杂度O(mn),空间复杂度O(mn)
    def minEdietDistince(self, s1, s2):
        meno = {} # 备忘录
        def dp(i, j):
            # base case
            if i == -1: return j + 1
            if j == -1: return i + 1

            if (i, j) in meno:
                return meno[(i, j)]

            if s1[i] == s2[j]:
                meno[(i, j)] = dp(i-1, j-1)
                return dp(i-1, j-1)  # 跳过
            else:
                meno[(i, j)] =  min(
                    dp(i, j-1) + 1,   # 插入
                    dp(i-1, j) + 1,   # 删除
                    dp(i-1, j-1) + 1  # 替换
                )
                return meno[(i, j)]
        return dp(len(s1)-1, len(s2)-1)
s1 = "intention"
s2 = "execution"
print(Solution2().minEdietDistince(s1, s2))

3) 动态规划优化——使用DP table
在这里插入图片描述

class Solution3:  # DP table,时间复杂度O(mn),空间复杂度O(mn)
    def minEdietDistince(self, s1, s2):
        m, n = len(s1), len(s2)
        if m * n == 0:
        	return m + n
        dp = [[0]*(n+1) for _ in range(m+1)]
        # base case
        for i in range(m+1):
            dp[i][0] = i
        for j in range(n+1):
            dp[0][j] = j
        # 自底向上求解
        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] = min(
                        dp[i-1][j] + 1,  # 删除
                        dp[i][j-1] + 1,  # 插入
                        dp[i-1][j-1] + 1  # 替换
                    )
        return dp[m][n]
s1 = "intention"
s2 = "execution"
print(Solution3().minEdietDistince(s1, s2))

4) 一维DP table
参考文章
我们看到虽然dp是二维数组,但我们计算的时候每个元素只和他的左边,上边,左上角的3个值有关,所以这里我们还可以优化一下,使用一维数组,我们看下代码。

class Solution4:  # DP table,时间复杂度O(mn),空间复杂度O(1)
    def minEdietDistince(self, s1, s2):
        m, n = len(s1), len(s2)
        if m * n == 0:
        	return m + n
        dp = [0] * (n+1)
        for i in range(n+1):
            dp[i] = i

        last = 0 # 记录左上方
        for i in range(1, m+1):
            last = dp[0]
            dp[0] = i
            for j in range(1, n+1):
                temp = dp[j]
                if s1[i-1] == s2[j-1]:
                    dp[j] = last
                else:
                    dp[j] = min(dp[j-1], dp[j], last) + 1
                last = temp
        return dp[n],dp
s1 = "intention"
s2 = "execution"
print(Solution4().minEdietDistince(s1, s2))

(5, [9, 8, 8, 8, 8, 8, 8, 7, 6, 5])

5) 增加追踪数组
最终返回最小操作次数(编辑距离)和操作记录数组

class Solution5:  # DP table,时间复杂度O(mn),空间复杂度O(mn)
    def minEdietDistince(self, s1, s2):
        m, n = len(s1), len(s2)
        if m * n == 0:
        	return m + n
        dp = [[0]*(n+1) for _ in range(m+1)]
        rec = [[0]*(n+1) for _ in range(m+1)]
        # base case
        for i in range(m+1):
            dp[i][0] = i
            rec[i][0] = "U"
        for j in range(n+1):
            dp[0][j] = j
            rec[0][j] = "L"
        # 自底向上求解
        for i in range(1, m+1):
            for j in range(1, n+1):
                c = 0
                if s1[i-1] != s2[j-1]:
                    c = 1
                replace = dp[i-1][j-1] + c
                delete = dp[i-1][j] + 1
                insert = dp[i][j-1] + 1
                if replace == min(replace, delete, insert):
                    dp[i][j] = dp[i-1][j-1] + c
                    rec[i][j] = "LU"
                elif insert == min(replace, delete, insert):
                    dp[i][j] = dp[i][j-1] + 1
                    rec[i][j] = "L"
                else:
                    dp[i][j] = dp[i-1][j] + 1
                    rec[i][j] = "U"
        return dp[m][n],rec
        ```
# 最优方案追踪,输出编辑的方法
def trackback(rec, s1, s2, m, n):
    if m == 0 or n == 0: return None
    if rec[m][n] == "LU":
        trackback(rec, s1, s2, m-1, n-1)
        if s1[m-1] == s2[n-1]:
            print("无需操作")
        else:
            print("将%s替换为%s"%(s1[m-1], s2[n-1]))
    elif rec[m][n] == "U":
        trackback(rec, s1, s2, m-1, n)
        print("删除%s" % s1[m-1])
    else:
        trackback(rec, s1, s2, m, n-1)
        print("插入%s" % s2[n-1])
#s1 = "intention"
#s2 = "execution"
s1 = 'horse'
s2 = 'ros'
m, n = len(s1), len(s2)
if __name__ == "__main__":
    res, rec = Solution5().minEdietDistince(s1, s2)
    print("将%s变为%s:" %(s1,s2))
    print("将%s变为%s的编辑方法:" %(s1,s2))
    print("操作次数(最小编辑距离):",res)
    print("--------追踪数组--------")
    for i in rec:
        print(i)
    trackback(rec, s1, s2, m, n)

下面是该方法的两个输入案例的结果:

将intention变为execution
操作次数(最小编辑距离): 5
--------追踪数组--------
[‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’, ‘L’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’, ‘LU’]
[‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘L’, ‘L’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘U’, ‘LU’, ‘L’]
[‘U’, ‘U’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘LU’, ‘U’, ‘U’, ‘LU’]
将intention变为execution的编辑方法:
将i替换为e
将n替换为x
将t替换为e
将e替换为c
将n替换为u
无需操作
无需操作
无需操作
无需操作

将horse变为ros
操作次数(最小编辑距离): 3
--------追踪数组--------
[‘L’, ‘L’, ‘L’, ‘L’]
[‘U’, ‘LU’, ‘LU’, ‘LU’]
[‘U’, ‘LU’, ‘LU’, ‘L’]
[‘U’, ‘LU’, ‘U’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘LU’]
[‘U’, ‘U’, ‘LU’, ‘U’]
将horse变为ros的编辑方法:
将h替换为r
无需操作
删除r
无需操作
删除e

参考文章及课程

  1. Leetcode72 https://leetcode-cn.com/problems/edit-distance
    2)经典动态规划:编辑距离 https://mp.weixin.qq.com/s/uWzSvWWI-bWAV3UANBtyOw
  2. 376,动态规划之编辑距离 https://mp.weixin.qq.com/s/ljrjbdIUAG50P2b0THknzw
  3. 算法设计与分析—童咏昕 https://www.icourse163.org/learn/BUAA-1449777166?tid=1463474515#/learn/announce
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

黄波波19

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

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

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

打赏作者

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

抵扣说明:

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

余额充值