LeetCode 213. 打家劫舍 II 【Python 分治法】

文章发布且更新于个人博客:https://www.xerrors.fun/leetcode/house-robber/

1. 题目

198. 打家劫舍

213. 打家劫舍 II

2. 分治法

198. 打家劫舍 I

使用分治法解决打家劫舍的问题。把整个序列进行切割,分为切割点偷或者不偷两种情况,这样就会得到一个线状的数组,数组的两端都含有偷与不偷的状态,所以每次求分支的时候都需要分节点处偷与不偷两种情况。并分别计算结果。

知道数组的长度为 1 或者 2 的时候,当节点长度为 1 的时候,如果两边的房间都没有被偷,此时就可以偷,否则不可以偷;当长度为 2 的时候,如果左边相邻的房间偷了而右边没偷,就偷右边的房间,相反亦然,当两个相邻房间都被偷的时候,就不可以偷。如果两边都没有偷,就可以选择其中最大的一个。

class Solution:
    def rob(self, nums: List[int]) -> int: 
        def div(nums, l_ind, r_ind, l_state, r_state):
            # 例子:左 1 2 右 此时两边界只差为 3
            
            # 边界之差为 2 说明中间只有一个房间没有遍历,能抢就抢,不能抢就算了
            if r_ind - l_ind == 2:
                if l_state or r_state:
                    return 0
                else:
                    return nums[l_ind+1]

            # 边界之差为 3 说明,左右边界之前有两个房间,所以需要分情况讨论
            elif r_ind - l_ind == 3:
                if l_state and r_state:
                    return 0
                elif l_state:
                    return nums[r_ind-1]
                elif r_state:
                    return nums[l_ind+1]
                else:
                    return max(nums[l_ind+1], nums[r_ind-1])
        	# 长度如果
            else:
                # 分为抢劫临界点和不抢劫临界点两种情况
                mid = (l_ind + r_ind) // 2

                # 不抢劫临界点
                l_sum_1 = div(nums, l_ind, mid, l_state, False)
                r_sum_1 = div(nums, mid, r_ind, False, r_state)
                sum_1 = l_sum_1 + r_sum_1
                
                # 抢劫临界点
                l_sum_2 = div(nums, l_ind, mid, l_state, True)
                r_sum_2 = div(nums, mid, r_ind, True, r_state)
                sum_2 = l_sum_2 + r_sum_2 + nums[mid]

                return max(sum_1, sum_2)

        if not nums:
            return 0 
        
        size = len(nums)

        if size == 1:
            return nums[0]

        return div(nums, -1, size, False, False)

image-20200622010611469

213. 打家劫舍 II

原本是一个环,所以可以对这个环进行一次切割,也就分为切割点偷与不偷两种情况,理解起来相对简单。

class Solution:
    def rob(self, nums: List[int]) -> int: 
        def div(nums, l_ind, r_ind, l_state, r_state):
            # 例子:左 1 2 右 此时两边界只差为 3
            
            # 边界之差为 2 说明中间只有一个房间没有遍历,能抢就抢,不能抢就算了
            if r_ind - l_ind == 2:
                if l_state or r_state:
                    return 0
                else:
                    return nums[l_ind+1]

            # 边界之差为 3 说明,左右边界之前有两个房间,所以需要分情况讨论
            elif r_ind - l_ind == 3:
                if l_state and r_state:
                    return 0
                elif l_state:
                    return nums[r_ind-1]
                elif r_state:
                    return nums[l_ind+1]
                else:
                    return max(nums[l_ind+1], nums[r_ind-1])
        	# 长度如果
            else:
                # 分为抢劫临界点和不抢劫临界点两种情况
                mid = (l_ind + r_ind) // 2

                # 不抢劫临界点
                l_sum_1 = div(nums, l_ind, mid, l_state, False)
                r_sum_1 = div(nums, mid, r_ind, False, r_state)
                sum_1 = l_sum_1 + r_sum_1
                
                # 抢劫临界点
                l_sum_2 = div(nums, l_ind, mid, l_state, True)
                r_sum_2 = div(nums, mid, r_ind, True, r_state)
                sum_2 = l_sum_2 + r_sum_2 + nums[mid]

                return max(sum_1, sum_2)

        if not nums:
            return 0 
        
        size = len(nums)

        if size == 1:
            return nums[0]

        a = div(nums, 0, size, False, False)
        b = div(nums, 0, size, True, True) + nums[0]
        return max(a, b)

从测试的结果来看,这个算法是没问题的。

image-20200622005841047

不过看起来还是有一点点的繁琐,结果当我看到别人的解法的时候我都懵了,就这几行???动态规划就是强!!!

class Solution:
    def rob(self, nums: [int]) -> int:
        def my_rob(nums):
            cur, pre = 0, 0
            for num in nums:
                cur, pre = max(pre + num, cur), cur
            return cur
        
        if len(nums) != 1:
            return max(my_rob(nums[:-1]),my_rob(nums[1:]))
        else:
            return nums[0]

3. 动态规划

这次又是我输了,选择去借鉴官方的解题思路,万万没想到这一次居然又是一个动态规划的题目,看来动态规划的知识要补一补了。下面先把打家劫舍 1 给做一下。这里是官方的思路,附带详细的视频解释:打家劫舍-力扣官方题解

资料:算法-动态规划 Dynamic Programming–从菜鸟到老鸟

关键还是在于找到最优子结构的问题,在考虑前 n 个房间的最大金额的时候,分为两种情况,第一种情况,打劫第n个房间,那么最大金额就等于前 n-2 个房间的最大金额以及第n个房间的金额之和。第二种情况不打劫第 n 个房间,那么最大金额就是前 n-1 个房间的最大金额。

通过上面的思路就把长度为 n 的问题转移到了长度更小的问题上面,把大的问题转移到了小问题的求解上面。所以状态转移方程如下:

sums[n] = max(sums[n-1], sums[n-2] + nums[n])

同时因为对每一个 n 的求解只需要 n-1 以及 n-2 的解,那么 sums 也就实际上没有必要使用一个数组来表示,直接使用三个变量来表示,官方说法这个叫滚动数组

class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0

        size = len(nums)
        if size == 1:
            return nums[0]
        
        first, second = nums[0], max(nums[0], nums[1])
        for i in range(2, size):
            first, second = second, max(first + nums[i], second)
        
        return second

image-20200622011310159

参考资料

[1] 算法-动态规划 Dynamic Programming–从菜鸟到老鸟 - CSDN

[2] 打家劫舍-力扣官方题解

[3] 打家劫舍 II(动态规划,结构化思路,清晰题解)- LeetCode

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值