目录
出自灵神的算法精讲:链接
1.打家劫舍
1.题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
2.输入格式
无
3.输出格式
无
4.样例输入
第一个:[1, 2, 3, 1]
第二个:[2, 7, 9, 3, 1]
5.样例输出
第一个:4
第二个:12
6.数据范围
1 <= nums.length <= 100
0 <= nums[i] <= 400
7.原题链接
2.解题思路
我们可以先把这题看成一个回溯题,把一个大问题变成规模更小的子问题,从第一个房子或者最后一个房子开始思考是最容易的,它们受到的约束最小。比如考虑最后一个房子选/不选,那么问题就变成n - 1个房子的问题,如果选,就变成n - 2个房子的问题,不断思考下去,就可以得到一棵搜索树。
比如4->2这条分支,如果选了第4个房子,相邻的房子不能选,所以直接递归到n - 2个房子。
把这个过程抽象化:当我们枚举到第i个房子选或者不选的时候,就确定了递归参数中的i。
我们将上述总结成三个问题:
- 当前操作:枚举第i个房子选/不选
- 子问题:从前i个房子中得到的最大金额和
- 下一个子问题(分类讨论):
- 不选:从前i - 1个房子中得到的最大金额和
- 选:从前i - 2个房子中得到的最大金额和
注意:
- ”第“和”前“两个关键字眼,在定义dfs或者dp数组的含义时,只能表示从一些元素中算出的结果,而不是从一个元素中算出的结果。
- 这里没有把得到的金额和作为递归的入参,而是把它当作了返回值,后面写记忆化搜索的时候就能明白了。
这样就可以得到
3.AC_code
朴素版(超时)
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)
优化成记忆化搜索
注意看这里算了两次dfs(2),这两次计算的结果都是一样的。我们可以把计算的结果存到一个cashe数组或者哈希表中,在第二次算的时候就可以直接返回cashe里面保存的结果了。
优化后的二叉树形式:
优化后的二叉树只有o(n)个节点,因此时间复杂度优化到了o(n)
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
cache = [-1] * n
def dfs(i):
if i < 0:
return 0
if cache[i] != -1:
return cache[i]
res = max(dfs(i - 1), dfs(i - 2) + nums[i])
cache[i] = res
return res
return dfs(n - 1)
优化成递推
注意看上面代码中的max函数,计算是发生在dfs调用结束后,也就是在递归的【归】过程中,才发生了实际的计算。既然我们知道递归的【归】的路径,就可以把【递】去掉,只剩下【归】的过程,也就是直接从下面开始往上计算。
1:1翻译成递推:
- dfs -> f数组
- 递归 -> 循环
- 递归边界 -> 数组初始值
防止数组下标越界,要对i = 0,i = 1的情况特殊处理,把i改成从2开始或者把i改成i + 2
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
f = [0] * (n + 2)
for i, x in enumerate(nums):
f[i + 2] = max(f[i + 1], f[i] + x)
return f[n + 1]