动态规划入门:从记忆化搜索到递推【基础算法精讲 17】_哔哩哔哩_bilibili
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
def dfs(i):
if i<0:
return 0
res = max(dfs(i-1),dfs(i-2)+nums[i])
return res
return dfs(n-1)
假设输入的 nums 列表为 [2, 7, 9, 3, 1]。
首先,函数 rob() 计算出列表的长度 n,即 n = 5。接着,它定义了一个名为 dfs() 的内部函数来实现深度优先搜索。
然后,dfs() 函数被调用,传入参数 i = n-1 = 4,表示从最后一间房子开始抢劫。由于这是深度优先搜索,dfs() 函数会递归地向左子树和右子树进行搜索,直到无法继续搜索或者找到最优解。
在第一次递归中,i = 3,dfs() 继续向左子树 dfs(i-1) 进行搜索。此时,i = 2,dfs() 再次向左子树进行搜索,i = 1 时再次向左子树进行搜索,i = 0 时终止搜索,返回结果为 2。
回到 i = 1 的那一层,dfs() 已经得到了它向左子树 dfs(i-1) (dfs(0)) 的结果为 2,现在需要计算其向右子树 dfs(i-2)+nums[i] 的结果。此时,i = 1,nums[1] = 7,所以右子树的值为 dfs(1-2) + 7,也就是 dfs(-1) + 7。但由于 dfs(-1) 会返回 0,所以右子树的值为 0 + 7 = 7。于是,dfs(1) 的返回结果就是 max(2, 7) = 7。
回到 i = 2 的那一层,由于左子树 dfs(i-1) 的值为 7,而右子树 dfs(i-2)+nums[i] 的值也为 11,所以 dfs(2) 的返回结果为 max(7, 11) = 11。
依此类推,最终得到的结果是 dfs(4) = max(dfs(3), dfs(2) + nums[4]) = max(max(dfs(2), dfs(1) + nums[3]), max(dfs(1), dfs(0) + nums[2]) + nums[4]) = max(max(9, 7+3), max(7, 0+9) + 1) = 12,表示可以在第一间房子和第三间房子抢劫,得到的总金额为 12。
回溯的复杂度是指数级别的,所以会超时,我们需要进行优化。
一 · 递归搜索 + 保存计算结果 = 记忆化搜索
class Solution:
def rob(self, nums: List[int]) -> int:
@cache
def dfs(i: int) -> int:
if i < 0: return 0
return max(dfs(i - 1), dfs(i - 2) + nums[i])
return dfs(len(nums) - 1)
@cache
是一个 Python 装饰器,用于缓存函数的结果。它可以将函数的输出结果缓存在内存中,以便在下次调用函数时,如果输入参数相同,则直接返回之前缓存的结果,而不必重新计算。@cache
装饰器只适用于那些输入参数可哈希的函数,否则会抛出 TypeError 异常。在这段函数里面,由于我们会重复计算递归函数的值,所以通过这个方法,就可以不需要重复计算,就比如在i = 1返回时候,已经计算了dfs(0)的值,但是在i = 2的时候还是会调用dfs(0),在这个时候就不需要再次计算,而是直接获取其值。
不使用装饰器
二 · 1:1 翻译成迭代
class Solution:
def rob(self, nums: List[int]) -> int:
f = [0] * (len(nums) + 2)
for i, x in enumerate(nums):
f[i + 2] = max(f[i + 1], f[i] + x)
return f[-1]
以 nums = [1, 2, 3, 1] 为例
首先,初始化一个长度为 len(nums) + 2 的数组 f,因为我们需要两个额外的位置来处理边界情况。所以现在 f = [0, 0, 0, 0, 0]。
然后,我们遍历 nums 中的每个元素,并将其与对应的索引一起传递给循环。第一次迭代中,i = 0,x = 1。
接下来,我们计算 f[i + 2] = max(f[i + 1], f[i] + x)。由于 i = 0,所以我们计算 f[2] = max(f[1], f[0] + 1)。由于 f[0] 和 f[1] 都等于 0,所以我们可以简化为 f[2] = max(0, 1) = 1。
下一次迭代中,i = 1,x = 2。现在我们需要计算 f[3] = max(f[2], f[1] + 2)。由于 f[2] 等于 1,而 f[1] 等于 0,所以我们可以简化为 f[3] = max(1, 0 + 2) = 2。
继续迭代,我们有 i = 2,x = 3。现在我们需要计算 f[4] = max(f[3], f[2] + 3)。由于 f[3] 等于 2,而 f[2] 等于 1,所以我们可以简化为 f[4] = max(2, 1 + 3) = 4。
最后一次迭代中,i = 3,x = 1。现在我们需要计算 f[5] = max(f[4], f[3] + 1)。由于 f[4] 等于 4,而 f[3] 等于 2,所以我们可以简化为 f[5] = max(4, 2 + 1) = 4。
因此,函数返回 f[-1],即 4,这是我们可以从 nums 中抢劫的最大金额。
三 · 空间优化
class Solution:
def rob(self, nums: List[int]) -> int:
f0 = f1 = 0
for i, x in enumerate(nums):
f0, f1 = f1, max(f1, f0 + x)
return f1
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
class Solution:
def rob1(self, nums: List[int]) -> int:
f0 = f1 = 0
for i, x in enumerate(nums):
f0, f1 = f1, max(f1, f0 + x)
return f1
def rob(self, nums: List[int]) -> int:
return max(nums[0] + self.rob1(nums[2:-1]), self.rob1(nums[1:]))