72. 编辑距离
问题描述
难度:困难
给你两个单词 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’)
提示:
0 <= word1.length, word2.length <= 500 word1 和 word2 由小写英文字母组成
解题思路
- 第一步:确定动态规划状态
这个题目涉及到两个字符串,所以我们最先想到就是用两维数组来保存转移状态,定义dp[i][j]
为字符串word1长度为i
和字符串word2长度为j
时,word1转化成word2所执行的最少操作次数的值。 - 第二步:写出状态转移方程
关于这个问题的状态转移方程其实很难想到,这里提供的一个方向就是试着举个例子,然后通过例子的变化记录每一步变化得到的最少次数,来找到删除,插入,替换操作的状态转移方程具体应该怎么写。
我们采用从末尾开始遍历word1
和word2
,
当word1[i]
等于word2[j]
时,说明两者完全一样,所以i
和j
指针可以任何操作都不做,用状态转移式子表示就是dp[i][j]=dp[i-1][j-1]
,也就是前一个状态和当前状态是一样的。
当word1[i]
和word2[j]
不相等时,就需要对三个操作进行递归了,这里就需要仔细思考状态转移方程的写法了。
对于插入操作,当我们在word1中插入一个和word2一样的字符,那么word2就被匹配了,所以可以直接表示为dp[i][j-1]+1
对于删除操作,直接表示为dp[i-1][j]+1
对于替换操作,直接表示为dp[i-1][j-1]+1
所以状态转移方程可以写成min(dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+1)
- 第三步:考虑初始化条件
我们还是利用dp转移表法来找到状态转移的变化(读者可以自行画一张dp表,具体方法在求最长子序列中已经演示过了),这里我们用空字符串来额外加入到word1和word2中,这样的目的是方便记录每一步操作,例如如果其中一个是空字符串,那么另外一个字符至少的操作数都是1,就从1开始计数操作数,以后每一步都执行插入操作,也就是当i=0
时,dp[0][j]=j
,同理可得,如果另外一个是空字符串,则对当前字符串执行删除操作就可以了,也就是dp[i][0]=i
。 - 第四步:考虑输出状态
在转移表中我们可以看到,可以从左上角一直遍历到左下角的值,所以最终的编辑距离就是最后一个状态的值,对应的就是dp[-1][-1]
。 - 第五步:考虑对时间,空间复杂度的优化
和上题一样,这里由于dp[i][j]
只和dp表中附近的三个状态(左边,右边和左上边)有关,所以同样可以进行压缩状态转移的空间存储,如果觉得有兴趣可以参考@Lyncien的解法,对于时间方面应该并没有可以优化的方法。
class Solution: def minDistance(self, word1: str, word2: str) -> int: m = len(word1) n = len(word2) # 构建二维数组dp数组 dp = [[0 for _ in range(n+1)] for _ in range(m+1)] # 考虑边界问题,第一行和第一列问题 for i in range(n+1): dp[0][i] = i for j in range(m+1): dp[j][0] = j for i in range(1,m+1): for j in range(1,n+1): if word1[i-1] == word2[j-1]: 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[-1][-1]
198.打家劫舍1
问题描述
难度:中等
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1: 输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2: 输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 =
9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。 提示: 1 <= nums.length <= 100 0 <= nums[i] <= 400
解题思路
这个问题不复杂,其实利用一般的迭代可以直接解出来,但是这里讲动态规划,所以还是按照标准的套路来
- 第一步:确定动态规划状态
直接定义题目所求的偷窃的最高金额,所以dp[i]
表示偷窃第i
号房子能得到的最高金额。 - 第二步:写出状态转移方程
如果我们不考虑限制条件相邻两个房子不能抢,那么问题就很简单。想得到第i
个房间偷窃到的最高金额的时候,我们会考虑子问题前i-1
间的最高金额dp[i-1]
,然后再加上当前房间的金额,所以最后可以表达为dp[i]=dp[i-1]+nums[i]
。
需要注意的是,这里限制了相邻两个房子是不能抢的,接下来我们就要考虑两种情况。
如果抢了第i个房间,那么第i-1
肯定是不能抢的,这个时候需要再往前一间,用第i-2
间的金额加上当前房间的金额,得到的状态转移方程是dp[i]=dp[i-2]+nums[i]
。
如果没有抢第i
个房间,那么肯定抢了第i-1
间的金额,所以直接有dp[i]=dp[i-1]
。
最后综合一下两种情况,就可以很快得到状态转移方程:dp[i]=max(dp[i-2]+nums[i],dp[i-1])
- 第三步:考虑初始化条件
初始化条件需要考虑第一个房子和第二个房子,之后的房子都可以按照规律直接求解,当我们只有一个房子的时候,自然只抢那间房子,当有两间房的时候,就抢金额较大的那间。综合起来就是dp[0]=nums[0],dp[1]=max(nums[0],nums[1])
。 - 第四步:考虑输出状态
直接返回状态转移数组的最后一个值就是所求的最大偷窃金额。 - 第五步:考虑对时间,空间复杂度的优化
时间复杂度为O ( N ) O(N)O(N)不能再优化了,空间复杂度方面如果用动态规划是不能优化,但是如果用迭代的方法只存储临时变量来记录每一步计算结果,这样可以降到O ( 1 ) O(1)O(1)。
class Solution: def rob(self, nums) -> int: if not nums: return 0 if len(nums)==1: return nums[0] n = len(nums) dp = [0] * n # 第一个边界值处理 # dp # i=2 # [1 2 (1+3,2) 0 0] # i=3 # [1 2 4 4 ] dp[0] = nums[0] dp[1] = max(nums[0],nums[1]) for i in range(2,n): dp[i] = max(dp[i-2]+nums[i],dp[i-1]) # [1,2,3,1] return dp[-1]
213_打家劫舍2
问题描述
难度:中等
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1: 输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 =
2), 因为他们是相邻的。
示例 2: 输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 =
1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3: 输入:nums = [0] 输出:0 提示: 1 <= nums.length <= 100 0 <= nums[i] <=
1000
- 第一步:确定动态规划状态
直接定义题目所求的偷窃的最高金额,所以dp[i]
表示偷窃第i
号房子能得到的最高金额。 - 第二步:写出状态转移方程和上个题目类似,这个题目不一样的是现在所有房屋都围成一个圈,相比于上个问题又增加了一个限制,这样一来第一个房子和最后一个房子只能选择其中一个偷窃了。所有我们把这个问题拆分成两个问题:
- 偷窃了第一个房子,此时对应的是
nums[1:]
,得到最大的金额value是v1
。 - 偷窃了最后一个房子,此时对应的是
nums[:n-1]
(其中n是所有房子的数量),得到的最大金额value是v2
。
最后的结果就是取这两种情况的最大值,即max(v1,v2)
。
每个子问题就和上题是一样的了,所以可以直接得到状态转移方程还是dp[i]=max(dp[i-2]+nums[i],dp[i-1])
- 第三步:考虑初始化条件
初始化一个房子和两个房子的情况就是dp[0]=nums[0],dp[1]=max(nums[0],nums[1])
。 - 第四步:考虑输出状态
直接返回状态转移数组的最后一个值就是所求的最大偷窃金额。 - 第五步:考虑对时间,空间复杂度的优化
时间复杂度为O ( N ) O(N)O(N)不能再优化了,空间复杂度方面如果用动态规划是不能优化,但是如果用迭代的方法只存储临时变量来记录每一步计算结果,这样可以降到O ( 1 ) O(1)O(1)。
class Solution: def rob(self, nums) -> int: if not nums: return 0 if len(nums) <= 2: return max(nums) def helper(nums): if len(nums) <= 2: return max(nums) dp = [0] *len(nums) dp[0] = nums[0] dp[1] = max(nums[0],nums[1]) for i in range(2,len(nums)): dp[i] = max(dp[i-1],dp[i-2]+nums[i]) return dp[-1] return max(helper(nums[1:]),helper(nums[:-1]))