一、题目描述

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

二、解题思路 & 代码

动态规划:
dp[i][j] 代表 word1i 位置转换成 word2j 位置需要最少步数

所以,

w o r d 1 [ i ] = = w o r d 2 [ j ] , d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] word1[i] == word2[j],dp[i][j] = dp[i-1][j-1] word1[i]==word2[j]dp[i][j]=dp[i1][j1]

w o r d 1 [ i ] ! = w o r d 2 [ j ] , d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j − 1 ] , d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 word1[i]!=word2[j]dp[i][j]=min(dp[i1][j1],dp[i1][j],dp[i][j1])+1

其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。

=======================================================
对“dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。”的补充理解

以 word1 为 “horse”,word2 为 “ros”,且 dp[5][3] 为例,即要将 word1的前 5 个字符转换为 word2的前 3 个字符,也就是将 horse 转换为 ros,因此有:

  1. dp[i-1][j-1],即先将 word1 的前 4 个字符 hors 转换为 word2 的前 2 个字符 ro,然后将第五个字符 word1[4](因为下标基数以 0 开始) 由 e 替换为 s(即替换为 word2 的第三个字符,word2[2])

  2. dp[i][j-1],即先将 word1 的前 5 个字符 horse 转换为 word2 的前 2 个字符 ro,然后在末尾补充一个 s,即插入操作

  3. dp[i-1][j],即先将 word1 的前 4 个字符 hors 转换为 word2 的前 3 个字符 ros,然后删除 word1 的第 5 个字符

=======================================================

注意,针对第一行,第一列要单独考虑,我们引入 '' '' 下图所示:

第一行,是 word1 为空变成 word2 最少步数,就是插入操作

第一列,是 word2 为空,需要的最少步数,就是删除操作

自底向上(迭代)

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n1 = len(word1)
        n2 = len(word2)
        dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]
        # 第一行
        for j in range(1, n2 + 1):
            dp[0][j] = dp[0][j-1] + 1
        # 第一列
        for i in range(1, n1 + 1):
            dp[i][0] = dp[i-1][0] + 1
        for i in range(1, n1 + 1):
            for j in range(1, n2 + 1):
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1
        #print(dp) 
        return dp[-1][-1]

复杂度分析

  1. 时间复杂度 :O(mn),其中 m 为 word1 的长度,n 为 word2 的长度。

  2. 空间复杂度 :O(mn),我们需要大小为 O(mn) 的 D 数组来记录状态值。

自顶向下(递归)

import functools
class Solution:
    @functools.lru_cache(None)
    def minDistance(self, word1: str, word2: str) -> int:
        if not word1 or not word2:
            return len(word1) + len(word2)
        if word1[0] == word2[0]:
            return self.minDistance(word1[1:], word2[1:])
        else:
            inserted = 1 + self.minDistance(word1, word2[1:])
            deleted = 1 + self.minDistance(word1[1:], word2)
            replace = 1 + self.minDistance(word1[1:], word2[1:])
            return min(inserted, deleted, replace)

由于字符串切片是 O(n),所以改成用了索引号。

这里的 @functools.lru_cache(None) 装饰器用来做缓存,他能把相对耗时的函数结果进行保存,避免传入相同的参数重复计算。同时,缓存并不会无限增长,不用的缓存会被释放。

尤其在充斥这大量重复计算时,它更能够为程序的运行节省大量的时间。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        import functools
        @functools.lru_cache(None)
        def helper(i, j):
            if i == len(word1) or j == len(word2):
                return len(word1) - i + len(word2) - j
            if word1[i] == word2[j]:
                return helper(i + 1, j + 1)
            else:
                inserted = helper(i, j + 1)
                deleted = helper(i + 1, j)
                replaced = helper(i + 1, j + 1)
                return min(inserted, deleted, replaced) + 1
        return helper(0, 0)
        

==========================================================

==========================================================

变形1. 分别输出每种编辑操作的次数

这个需要用递归,也就是上面的第二种方法,自顶向下backtrack

代码:

========= 略 ==========

关于 @functools.lru_cache()
@functools.lru_cache()
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
    print(fibonacci(6))

不使用lru_cache装饰器时的运行时间是0.00011096s,加上以后变成了0.00006876s

参考:functools.lru_cache装饰器详解

==========================================================

==========================================================

变形2. n次最短编辑距离

题目描述:
给定n个长度不超过10的字符串以及m次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的n个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

输入格式

第一行包含两个整数n和m。
接下来n行,每行包含一个字符串,表示给定的字符串。
再接下来m行,每行包含一个字符串和一个整数,表示一次询问。
字符串中只包含小写字母,且长度均不超过10。

输出格式

输出共m行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。

数据范围

1≤n,m≤1000,

输入样例:

3 2
abc
acd
bcd
ab 1
acbd 2

输出样例:

1
3

解题思路
最短编辑距离的应用, 求n次最短编辑距离

Python 代码

def can_solve(a, b, t):
    """ 求字符串a是否能在t的次数内编辑到b 为了方便计算, a和b在前面随便加个字符占位 """
    a = ' ' + a
    b = ' ' + b
    la, lb = len(a), len(b)
    dp = [[0] * lb for _ in range(la)]

    # 初始化:a[i]到空字符串的编辑距离为i, 空字符串到b[i]的编辑距离也是i
    for i in range(la):
        dp[i][0] = i
    for i in range(lb):
        dp[0][i] = i

    for i in range(1, la):
        for j in range(1, lb):
            # dp[i][j]: a的前i个字母到b的前j个字母的最短编辑距离
            # 枚举最后一步的编辑方式: 增 dp[i - 1][j] + 1, 删 dp[i][j - 1] + 1, 改dp[i - 1][j - 1] + 1
            # 如果a[i] == b[j] 不需要编辑, 直接是dp[i - 1][j - 1]
            dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i] != b[j]))

    return dp[la - 1][lb - 1] <= t

def main():
    n, m = [int(x) for x in input().split()]
    s = []
    for _ in range(n):
        s.append(input())
    for _ in range(m):
        q, t = input().split()
        t = int(t)
        res = 0
        for i in range(n):
            res += can_solve(s[i], q, t)
        print(res)

main()

参考: 【算法学习】AcWing 899. 编辑距离(dp)

参考:

  1. LeetCode题解