【力扣日记】打家劫舍! 198,213,337 | 动态规划

198 打家劫舍 I

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

算法思路

动态规划!!重点在于状态转移方程
先初始化dp数组:dp=[0]*len(nums)
简单的考虑,从1开始:
仅有一所房子,dp[0]=sums[0]
有两所房子,dp[1]=max(sums)
第三所房子,面临着两种情况:

  1. 偷:偷第三所房子,意味着不能偷第二个,此时金额为sums[0]+sums[2]
  2. 不偷:此时金额为dp[1]=max(sums[0:2])
显然:dp[2]=max(dp[1],dp[0]+sums[2])
推广:dp[i]=max(dp[i-2]+nums[i],dp[i-1])

得到状态转移方程,又有了dp的初始状态,即可得到算法

算法

必须吐槽的是为什么测试里还有sums=[]???
这已经不是家里有没有钱的问题了,你连房子都不给我你就让我去偷东西?

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:return 0
        if len(nums)<3:return max(nums)
        dp=[0]*len(nums)
        dp[0],dp[1]=nums[0],max(nums[0],nums[1])
        for i in range(2,len(nums)):
            dp[i] = max(dp[i-2]+nums[i], dp[i-1])
        return dp[-1]

2020/05/29 打卡

今天回顾这道题,才发现还可以优化一下,把特殊情况给归到正常状态里。
给dp数组加上[0,0]即可将三种特殊情况一次性解决。

class Solution:
    def rob(self, nums: List[int]) -> int:
        dp=[0]*(len(nums)+2)
        for i in range(2,len(nums)+2):
            dp[i]=max(dp[i-2]+nums[i-2],dp[i-1])
        return dp[-1]

执行用时 :36 ms, 在所有 Python3 提交中击败了85.26%的用户

213 打家劫舍 II

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

算法思路

和198类似,区别是房子首尾相连,被提醒到只有两种情况:

  1. 偷第一个房子,那么一定不能偷最后一个
  2. 从第二个房子开始,那么可以偷最后一个

算法

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:return 0
        if len(nums)<3:return max(nums)
        dp=[0]*len(nums)
        dp[0],dp[1]=nums[0],max(nums[0],nums[1])
        for i in range(2,len(nums)-1):
            dp[i] = max(dp[i-2]+nums[i], dp[i-1])
        MAX=dp[-2]
        dp[1],dp[2]=nums[1],max(nums[1],nums[2])
        for i in range(3,len(nums)):
            dp[i] = max(dp[i-2]+nums[i], dp[i-1])
        MAX=max(dp[-1],MAX)
        return MAX

优化:

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:return 0
        length = len(nums)
        if length <3:return max(nums)

        def helper(nums):
            dp=[0]*len(nums)
            dp[0],dp[1]=nums[0],max(nums[0],nums[1])
            for i in range(2,len(nums)):
                dp[i] = max(dp[i-2]+nums[i], dp[i-1])
            return dp[-1]

        return max(helper(nums[:-1]), helper(nums[1:]))

执行用时 :36 ms, 在所有 Python3 提交中击败了63.93%的用户
内存消耗 :13.4 MB, 在所有 Python3 提交中击败了32.85%的用户

337 打家劫舍 III

题目描述

在上次打劫完一条街道之后和一圈房屋后剧情居然接上了小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

算法思路

冥思苦想好半天,越想越复杂,一看大佬题解,豁然开朗。

1、暴力递归

这里我们建模如:对一个根节点、左右子节点和最多四个孙节点,有偷了根就不能偷子。
所以对根节点来说,所能获得的最大数额是:
根节点+所有孙子节点 | 子节点之和:这两部分中更大的那个。

max(root.val+self.rob(root.left.left) + self.rob(root.left.right)+self.rob(root.right.left) + self.rob(root.right.right),self.rob(root.left)+self.rob(root.right))

由此写出递归函数:

class Solution:
    def rob(self, root: TreeNode) -> int:
        if not root:return 0
        money=root.val
        if root.left:money+=self.rob(root.left.left) + self.rob(root.left.right)
        if root.right:money+=self.rob(root.right.left) + self.rob(root.right.right)
        return max(money,self.rob(root.left)+self.rob(root.right))

但是python版本因为重复计算超时了。
分析易知:计算根节点时就需要计算子节点与孙节点,当子节点作为根节点时就会重复计算孙节点的部分……

优化:

2、缓存表递归

给递归的函数添加一个字典保存已计算的的节点值

class Solution:
    def rob(self, root: TreeNode,di={
     }) -> int:
        if not root:return 0
        if di.get(root,None):return di[root]
        money=root.val
        if root.left:money+=self.rob(root.left.left,di) + self.rob(root.left.right,di)
        if root.right:money+=self.rob(root.right.left,di) + self.rob(root.right.right,di)
        rec=max(money,self.rob(root.left,di)+self.rob(root.right,di))
        di[root]=rec
        return rec

执行用时 :68 ms, 在所有 Python3 提交中击败了33.66%的用户
内存消耗 :17.1 MB, 在所有 Python3 提交中击败了5.34%的用户

3、动态规划

我们换一种办法来定义此问题:
我们使用一个大小为2的数组来表示 res = [0,0] 0代表不偷,1代表偷
任何一个节点能偷到的最大钱的状态可以定义为:

  1. 当前节点选择不偷: 当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
  2. 当前节点选择偷: 当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
    _
    需要指出的根节点不偷时,子节点也可以选择不偷。
    二叉树如:[1,200,3,null,3,null,100]就是只偷左子树与右孙子树。

——
如上定义转化为公式:

root[0] = max(rob(root.left)[0], rob(root.left)[1]) + max(rob(root.right)[0], rob(root.right)[1])
root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
class Solution:
    def rob(self, root: TreeNode) -> int:
        if not root:return 0
        def robber(root):
            if not root: return [0,0]
            left = robber(root.left);
            right = robber(root.right);

            result1=max(left[0], left[1]) + max(right[0], right[1])
            result2 = left[0] + right[0] + root.val
            return [result1,result2]
        return max(robber(root))

执行用时 :56 ms, 在所有 Python3 提交中击败了66.71%的用户
内存消耗 :15.6 MB, 在所有 Python3 提交中击败了35.59%的用户

class Solution:
    def rob(self, root: TreeNode) -> int:
        if root==None:
            return 0
        def helper(root):
            if root==None:
                return [0,0]
            left = helper(root.left)
            right = helper(root.right)
            rob = root.val + left[1] + right[1]
            skip = max(left) + max(right)
            return [rob, skip]
        res = helper(root)
        return max(res)

参考

三种方法解决树形动态规划问题-从入门级代码到高效树形动态规划代码实现

你可能感兴趣的:(力扣日记,leetcode,python,动态规划)