文章发布且更新于个人博客:https://www.xerrors.fun/leetcode/house-robber/
1. 题目
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)
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)
从测试的结果来看,这个算法是没问题的。
不过看起来还是有一点点的繁琐,结果当我看到别人的解法的时候我都懵了,就这几行???动态规划就是强!!!
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
参考资料
[1] 算法-动态规划 Dynamic Programming–从菜鸟到老鸟 - CSDN
[2] 打家劫舍-力扣官方题解