摘要
本文主要介绍了LeetCode动态规划的几个题目,包括198.打家劫舍、213.打家劫舍II、337.打家劫舍III。
1、198.打家劫舍
1.1 思路
动规五部曲
-
dp数组以及下标的含义- dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i] 。
-
递推公式
-
决定dp[i]的因素就是第i房间偷还是不偷
- 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i]
- 如果不偷第i房间,那么dp[i] = dp[i - 1]
-
然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
-
-
dp数组如何初始化-
从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]
- dp[0] 一定是 nums[0]
- dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
-
-
dp数组遍历顺序- dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
-
举例推导
dp数组
1.2 代码
public int rob(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
dp[0] = nums[0];
for(int i=1; i<len; i++) {
if(i == 1) {
dp[i] = Math.max(nums[0], nums[1]);
continue;
}
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[len-1];
}
2、213.打家劫舍II
2.1 思路
-
思路
-
对于一个数组,成环的话主要有如下两种情况:
-
情况一:考虑包含首元素,不包含尾元素
-
情况二:考虑包含尾元素,不包含首元素
-
-
分析到这里,本题其实比较简单了。 剩下的和198.打家劫舍就是一样的了
-
-
注意事项
-
dp数组初始化大小仍然为nums.length而不是end-start,以避免出现空指针异常int[] dp = new int[nums.length];
-
2.2 代码
public int rob(int[] nums) {
int len = nums.length;
if(len == 1) {
return nums[0];
}
return Math.max(doRob(nums, 0, len - 1), doRob(nums, 1, len));
}
// 左闭右开
public int doRob(int[] nums, int start, int end) {
int len = end - start;
int[] dp = new int[nums.length];
dp[start] = nums[start];
for(int i=start+1; i<end; i++) {
if(i == start+1) {
dp[i] = Math.max(nums[start], nums[start+1]);
continue;
}
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[end-1];
}
3、337.打家劫舍III
3.1 思路
-
思路
- 本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。
- 与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。
- 如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑” )
-
动态规划
- 动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱
-
递归
-
确定递归函数的参数和返回值
-
其实这里的返回数组就是dp数组,所以dp数组(dp table)以及下标的含义:
- 下标为0记录不偷该节点所得到的的最大金钱
- 下标为1记录偷该节点所得到的的最大金钱。
-
-
确定终止条件
- 在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回。 这也相当于dp数组的初始化 。
-
确定遍历顺序
-
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。
- 通过递归左节点,得到左节点偷与不偷的金钱。
- 通过递归右节点,得到右节点偷与不偷的金钱
-
-
单层递归逻辑
-
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的
val0 = max(left[0], left[1]) + max(right[0], right[1]);
-
如果是偷当前节点,那么左右孩子就不能偷
val1 = cur->val + left[0] + right[0];
-
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
-
-
3.2 代码
public int rob(TreeNode root) {
int[] arr = doRob(root);
return Math.max(arr[0], arr[1]);
}
public int[] doRob(TreeNode root) {
if(root == null) {
return new int[]{0, 0};
}
int[] left = doRob(root.left);
int[] right = doRob(root.right);
int val0 = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
int val1 = root.val + left[0] + right[0];
return new int[]{val0, val1};
}