LeetCodeCampsDay39动态规划part07

198. 打家劫舍

https://leetcode.cn/problems/house-robber/

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

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

示例 1:

1
2
3
4
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

1
2
3
4
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

动态规划思路

明显是个01背包问题

  1. dp数组[i]表示拿到第i家后一共有dp[i]金额

  2. 递推公式:每家只有能拿和不能拿,如果能拿,则dp[i]=dp[i - 2] + val[i];如果不能拿则dp[i]=dp[i-1];所以需要判断拿与不拿的最大值

  3. 初始化,dp[n + 1]全初始化为0,但dp[1]初始化为nums[0],因为至少需要抢一家吧;(其实可以把dp[2]也初始化了,赋值为nums[1],但也可以不做,在遍历时会自动赋值的)

  4. 遍历顺序,只用一层循环,只遍历每家

  5. 举例:

  6. nums = [ 2, 7, 9, 3, 1]

    dp = [0, 2, 0, 0, 0, 0]

    dp = [0, 2, 7, 11, 11, 12]

动态规划代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def rob(self, nums: List[int]) -> int:
# 01背包问题,
# dp数组[i]表示拿到第i家后一共有dp[i]金额

# 递推公式:
# 每家只有能拿和不能拿,如果能拿,则dp[i]=max(dp[i-1], dp[i - 2] + val[i])

# dp初始化
# dp[n + 1],刚开始全部初始化为0
L = len(nums)
dp = [0] * (L + 1)
dp[1] = nums[0]
# 遍历顺序: 只遍历物品就可以了吧
for i in range(2, L + 1):
# 注意i-1才对应nums的下标
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1])

return dp[-1]

# 举例
# nums = [ 2, 7, 9, 3, 1]
# dp = [0, 2, 0, 0, 0, 0]
# dp = [0, 2, 7, 11, 11, 12]

213. 打家劫舍 II

https://leetcode.cn/problems/house-robber-ii/

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

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

示例 1:

1
2
3
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

1
2
3
4
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

1
2
输入:nums = [1,2,3]
输出:3

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

动态规划思路

这道题目和198.打家劫舍 (opens new window)是差不多的,唯一区别就是成环了。

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素

213.打家劫舍II

213.打家劫舍II

  • 情况二:考虑包含首元素,不包含尾元素

213.打家劫舍II1

213.打家劫舍II1

  • 情况三:考虑包含尾元素,不包含首元素

213.打家劫舍II2

213.打家劫舍II2

注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。

而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。

分析到这里,本题其实比较简单了。

剩下的和198.打家劫舍就是一样的了。

动态规划代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution:
def rob(self, nums: List[int]) -> int:
# 本题注意是围成一圈表示首尾相接,例如nums=[1, 2, 3]如果拿了3则不能拿1

# 本题可以套用普通的打家I


def foo(nums: List[int]) -> int:
L = len(nums)
dp = [0] * (L + 1)
dp[1] = nums[0]
# 遍历顺序: 只遍历物品就可以了吧
for i in range(2, L + 1):
# 注意i-1才对应nums的下标
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1])

return dp[-1]

if len(nums) == 1:
return nums[0]
if len(nums) == 0:
return 0

res1 = foo(nums[0:-1])
res2 = foo(nums[1:])
return max(res1, res2)

337. 打家劫舍 III

https://leetcode.cn/problems/house-robber-iii/

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

示例 1:

img

1
2
3
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

img

1
2
3
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104

动态规划思路

这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解

  1. 确定递归函数的参数和返回值

这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。

参数为当前节点,代码如下:

1
def robTree (node: TreeNode) -> Tuple[int]:

其实这里的返回数组就是dp数组。

所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

长度为2的数组怎么标记树中每个节点的状态呢?

别忘了在递归的过程中,系统栈会保存每一层递归的参数

  1. 确定终止条件

在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

1
if (cur == NULL) return vector<int>{0, 0};

这也相当于dp数组的初始化

  1. 确定遍历顺序

首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。

通过递归左节点,得到左节点偷与不偷的金钱。

通过递归右节点,得到右节点偷与不偷的金钱。

代码如下:

1
2
3
4
5
# 下标0:不偷,下标1:偷
# 先遍历左、右子树
resLeft = self.foo(node.left)
resRight = self.foo(node.right)

  1. 确定单层递归的逻辑

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是(val1, val2); 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

代码如下:

1
2
3
4
5
6
7
8
# 再遍历中节点

# 不偷root节点,则再考虑左、右子树分别的最大值
val1 = max(resLeft[0], resLeft[1]) + max(resRight[0], resRight[1])
# 偷root节点,则不偷左、右子树
val2 = node.val + resLeft[0] + resRight[0]

return (val1, val2)
  1. 举例推导dp数组

以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导

img

最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱

动态规划代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def foo(self, node: TreeNode) -> (int):
# 空节点则返回金额(0, 0)表示偷或不偷都是0
# 定义dp[0]为不偷,dp[1]为偷
if node is None:
return (0, 0)

# 使用后序遍历
resLeft = self.foo(node.left)
resRight = self.foo(node.right)

# root节点处理子树

# 不偷root节点,则再考虑左、右子树分别的最大值
val1 = max(resLeft[0], resLeft[1]) + max(resRight[0], resRight[1])

# 偷root节点,则不偷左、右子树
val2 = node.val + resLeft[0] + resRight[0]

return (val1, val2)

def rob(self, root: Optional[TreeNode]) -> int:
res = self.foo(root)
return max(res)