【LeetCode 198】打家劫舍——Python解决

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

2. 分析

只要偷的房子不相邻,并且宝物值最大即可。

暴力解法: 检查所有房子的组合,对每一个组合,检查是否有相邻的房子,如果没有,记录其价值。找最大值。时间复杂度为 O((2^n)*n)

这个问题并不是让你求所有的组合,而是让你求最大值,换句话说是个最优化问题,这类问题的解空间本身就是一个组合的解空间,很容易想到使用递归去解决
【LeetCode 198】打家劫舍——Python解决_第1张图片
每个问题都是在求解一个最优化的值,子问题的最优化的值配合上当前的决策在每一步中选择一个最大值就能得到原问题的最优解,故它也拥有最优子结构问题
【LeetCode 198】打家劫舍——Python解决_第2张图片
设置了一个函数f,f里的参数实际上就是状态定义的x,初始化时整个问题就是在求f(0),换句话说就是考虑偷取[0……n-1]范围里的房子,能获得的最大值是多少,为了获取这个最大值,就要考虑这些情况。v(0)代表取出的0号房子所代表的价值,然后将该值+从[2……n-1]范围里的房子就代表一种情况……

状态实际上定义了这个函数要做什么,状态转移定义了这个函数怎么做

3. 第一种方法——记忆化搜索

要做一个递归函数tryRob,这个状态就是考虑尝试去盗取这个街上的房子,是从第0个房子开始考虑的:tryRob(nums, 0)tryRob中传入nums和最初开始考虑的索引index,这个函数就是考虑抢劫nums[index…len(nums))这个范围内的所有房子。

  • 首先考虑递归终止条件:若要考虑的index已经大于等于len(nums)了,也就是考虑的是一个空集了,直接return 0即可。
  • 否则就要尝试在考虑的范围内去抢劫房子:for i in range(index, len(nums)),每次就是尝试抢劫nums[i]这个房子,抢劫完这个房子后,还要继续调用tryRob去尝试抢劫nums这个数组中从 i+2 这个索引位置的房子开始考虑,所以在这个循环遍历中有 nums[i] + tryRob(nums, i + 2) 的抢劫方案,我们要从这些方案中选出最优的,所以我们初始化res = float('-inf'),每次都要更新一下res的最大值,res = max(res, nums[i] + tryRob(nums, i + 2)),最后返回res即可。

很明显这里面是有重叠子问题的,所以考虑使用记忆化搜索

  • 初始化一个memo,有n个房子,全部初始化为-1:memo = [-1 for i in range(len(nums))](memo[i]表示考虑抢劫nums[i……n-1]所能获得的最大收益)
  • 在递归函数中,递归出口不用变,接下来若 memo[index] 有值并且不等于-1,直接返回 memo[index] 即可;否则才要进行下面的计算,计算完毕后还要更新 memo[index] 的值:memo[index] = res,也就是将res的值记录下来,再返回res即可。
class Solution:
    def rob(self, nums: List[int]) -> int:
        self.n = len(nums)
        self.memo = [-1 for i in range(self.n)]
        return self.tryRob(nums, 0)

    def tryRob(self, nums, index):
        if index >= self.n:
            return 0
        if self.memo[index] != -1:
            return self.memo[index]
        res = float('-inf')
        for i in range(index, self.n):
            res = max(res, nums[i] + self.tryRob(nums, i + 2))
        self.memo[index] = res
        return res

4. 第二种方法——动态规划

对于动态规划来说,需要有一个数组memo来记录每一个子问题相应的结果memo = [-1 for i in range(len(nums))](memo[i]表示考虑抢劫nums[i……n-1]所能获得的最大收益)

  • 首先解决最基础的子问题,memo[n-1]就表示这一个房子(从n-1到n-1只有一个房子),只偷这一个就完了:memo[n-1] = nums[n-1]此处有个小陷阱访问数组中的某一个索引时就要小心数组越界的问题(因为要执行memo[n-1],若n=0,就是memo[-1],会出错),故在前面求得数组长度n之后要判断n是否为0,若是直接返回0即可。
  • 接下来再看如何递推,for i in range(n-2, -1, -1),i从n-2开始,一直减到0,最后求得memo[0]就是整个问题的整体。如何求memo[i],还需要一层循环 for j in range(i, n), 每一次的memo[i]都是取最大值memo[i] = max(memo[i], nums[j] + memo[j+2])(尝试偷取j索引代表的房子,之后再考虑从j+2一直到n-1这个范围内的房子),此处j+2是需要判断的,防止越界,写为nums[j] + (memo[j + 2] if j + 2 < n else 0),通过这样的状态转移方程求出的memo[0]就是整个问题的解,最后返回memo[0]即可。
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0:
            return 0
        memo = [-1 for i in range(n)]
        memo[n - 1] = nums[n - 1]
        for i in range(n - 2, -1, -1):
            for j in range(i, n):
                memo[i] = max(memo[i], nums[j] + (memo[j + 2] if j + 2 < n else 0))
        return memo[0]

5. 改变状态转移方程后的代码

很多时候可以设立不同的状态,得到同样正确的答案。
【LeetCode 198】打家劫舍——Python解决_第3张图片

此处将状态的定义改为:考虑偷取[0……x]范围里的房子;
状态转移方程就变成了 f(n-1) = max{v(n-1)+f(n-3),v(n-2)+f(n-4), v(n-3)+f(n-5),……,v(3)+f(1), v(2)+f(0), v(1), v(0)}

  • 记忆化搜索
class Solution:
    def rob(self, nums: List[int]) -> int:
        self.n = len(nums)
        self.memo = [-1 for i in range(self.n)]
        return self.tryRob(nums, self.n - 1)

    def tryRob(self, nums, index):
        if index < 0:
            return 0
        if self.memo[index] != -1:
            return self.memo[index]
        res = float('-inf')
        for i in range(index, -1, -1):
            res = max(res, nums[i] + self.tryRob(nums, i - 2))
        self.memo[index] = res
        return res
  • 动态规划
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 0:
            return 0
        memo = [-1 for i in range(n)]
        memo[0] = nums[0]
        for i in range(1, n):
            for j in range(0, i + 1):
                memo[i] = max(memo[i], nums[j] + (memo[j - 2] if j - 2 >= 0 else 0))
        return memo[-1]

你可能感兴趣的:(LeetCode刷题,leetcode,动态规划,python)