动态规划经典教学题,上过《算导》的应该都会
- 2020 年 5 月 28 日
- 筆記
- leetcode, LeetCode题解, 动态规划, 算法
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是LeetCode专题第41篇文章,我们一起来看一道经典的动态规划问题Edit Distance,编辑距离。
今天这道题我本来是想跳过的,因为它实在是太经典了,属于典型的老掉牙问题了。但是想了想,一方面因为之前立了flag要把所有Medium和Hard写一遍,另一方面也是为了照顾萌新,所以还是把这题放上来了。相信上过算法导论这门课的同学一定都见过它,如果你没有上过属于萌新,那也没有关系,学习起来也不会很费劲的。
编辑距离
编辑距离非常经典,它指的是我们要花费多少力气将一个字符串A变成字符串B。我们需要花费的力气越多,那么说明这两个字符串相差得越大,如果我们花费很少,说明两个字符串很接近。所以它可以用来作为衡量两个字符串相似程度的依据,今天这道题的题面就是要求两个字符串的编辑距离。
前面说了编辑距离就是我们将字符串A通过编辑变成字符串B花费的力气,但是力气是一个主观的概念,我们需要将它量化。量化的方式也很简单,我们规定删除一个字符、添加一个字符和修改一个字符的花费都是1。整体编辑距离就是所有编辑操作的花费之和。
比如我们把horse变成rose,显然只需要删除ho并且插入一个o即可,所以编辑距离是3。
我们来看一个题目中的样例:
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')
题解
我们拿到这道题本能地会想要从头部开始依次比对来寻找答案,但是很快就能发现这样是无法找到最优解的。原因也很简单,因为我们在遍历的过程当中没办法开天眼看到后面的情况。举个简单的例子,比如说我们现在有两个字符串,分别是aabbc和aabcc。
当我们发现b和c不匹配的时候,我们是不知道究竟是插入一个c还是删除b还是把b转变成c可以得到最优的结果的,因为我们不知道这两个位置后面的位置是怎样的。究竟当下如何决策对未来更好,我们只能得到当下尽可能好的决策,无法预测未来。所以贪心是不行的,直接进行运算也是不行的。
那要怎么做呢?显然到了这个时候,摆在我们面前的可行方案只剩下了动态规划这一种了。
在这道问题当中,动态规划的思想除了状态转移之外更加类似于递推或者是记忆化搜索。记忆化搜索的意思是,虽然我们每一个状态都有若干个决策,这些决策组合之后的样本空间是非常庞大的,但是这些决策得到的结果数量是有限的,都可以归纳到某一个较小的范畴内。
举个例子,比如A串的长度是5,B串的长度是3。我们对A串进行若干次编辑之后得到了B串,这当中编辑的种类是无穷的。因为我们可以随意插入任意个字符,然后再将它们删除,这都可以被视为一种解。但是这些操作得到的状态是固定的,都是A串和B串相等了。那么我们只需要记录这个最终状态,也就是A串长度5变成了B串长度3的这个状态的最优解。
明确了这个之后,我们就可以愉快地递推了。
因为对于A5B3的状态来说,它并不是孤立的,它是可以从其他状态推导得到的。比如A4B3,比如A5B2。如果我们保存了这些所有的子状态的最优解,那么我们就可以从其中找到使得当下最优的决策。其实它就是搜索的思想,我们要得到A5B3,需要从A4B3和A5B2当中找到一个最优的,那么按道理我们又应该取搜索A4B3和A5B2,但是这是没有必要的,因为它们之间存在逻辑上的包含关系,A5B2和A4B3在计算A5B3的时候已经计算过了,所以我们把它保存起来,直接查询即可。
在这个问题当中,显然我们用数组保存即可。如果从递推的思路出发那么这个就是一个递推的问题,我们当前的状态是从其他状态推导得到的,我们寻找最优的递推关系。如果从搜索的思路出发就是搜索每一个状态的最佳答案,为了优化搜索效率,我们记录下中途得到每一个状态的最优解。
无论怎么理解,逻辑都是类似的,我们从局部最优推导整体最优。只不过用状态方程来表示更加清晰,我们用dp[i][j]表示A串前i个字符编辑成B串前j个字符的编辑距离。如果A[i] 和B[j]不等,那么有三种方式可以得到相等。第一种是删除i这个位置,第二种是插入B[j]这个字符,第三种是将A[i]编辑成B[j]。
如果删除A[i]的话,上一个状态是dp[i-1][j],如果插入B[j],那么上一个状态是dp[i][j-1]。如果是第三种,那么上一个状态是dp[i-1][j-1]。我们不知道这三种情况哪一种是最优的,所以都要考虑,通过min从其中选一个最小值。然后加上这次编辑带来的消耗1。
如果i和j位置相等,那么最优解一定是dp[i-1][j-1]。如果这些都理解了,代码就是几行的事情。
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n, m = len(word1), len(word2)
if n == 0 or m == 0:
return max(n, m)
# 初始化为无穷大
dp = [[0x3f3f3f3f for _ in range(m+2)] for _ in range(n+2)]
# 由于下标从1开始,所以把字符串增长一位
word1 = ' ' + word1
word2 = ' ' + word2
# 初始化dp[0][i], dp[i][0]都等于i
# 相当于填充i个字符
dp[0][0] = 0
for i in range(1, n+1):
dp[i][0] = i
for j in range(1, m+1):
dp[0][j] = j
for i in range(1, n+1):
for j in range(1, m+1):
# 状态转移方程
if word1[i] == word2[j]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
return dp[n][m]
算法导论在讲解动态规划的时候就是用这题做的例题,所以说真的非常的经典。没有做过的同学一定不能错过。
文章到这里就结束了,如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。