题目列表
解题过程
1、198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路: 当前状态和前面状态会有一种依赖关系,反映在动规的递推公式上。
动态规划五部曲:
- 确定dp数组以及下标的含义
- dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
- 确定递推公式
- 决定dp[i]的因素就是第i房间偷还是不偷
- dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])。
- dp数组如何初始化
- dp[0] = nums[0];
- dp[1] = max(nums[0], nums[1])。
- 确定遍历顺序
- 一层for循环;
- dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
- 举例推导dp数组
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.length - 1];
}
}
2、213.打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
思路: 这里,和上一题的区别是成环了。这里考虑三种情况(实际两种)(展开成直线):
- 考虑不包含首尾元素
- 考虑包含首元素,不包含尾元素
- 考虑包含尾元素,不包含首元素
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
return Math.max(robAction(nums, 0, nums.length - 1), robAction(nums, 1, nums.length));
}
int robAction(int[] nums, int start, int end) {
// 简单写法,不用创建dp数组
int x = 0, y = 0, z = 0;
for (int i = start; i < end; i++) {
y = z; // dp[i - 1]
z = Math.max(y, x + nums[i]);
x = y; //dp[i - 2]
}
return z;
}
}
3、337.打家劫舍III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
思路: 本题需要使用后序遍历,因为要通过递归函数的返回值来做下一步计算。如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以 考虑 抢左右孩子。
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
- 确定递归函数的参数和返回值
- 返回值:长度为2的数组 -> 一个节点偷与不偷的两个状态所得到的金钱
- dp数组:下标为0记录不偷该节点所得到的最大金钱,下标为1记录偷该节点所得到的最大金钱
- 确定终止条件
- 遇到空节点
- 确定遍历顺序
- 使用后序遍历,通过递归函数返回值做下一步计算。
- 通过递归左节点,得到左节点偷与不偷的金钱。
- 通过递归右节点,得到右节点偷与不偷的金钱。
- 确定单层递归的逻辑
- 如果是偷当前节点,那么左右孩子都不能偷,val1 = cur->val + left[0] + right[0];
- 如果不偷当前节点,那么左右孩子就可以偷,选偷与不偷中最大的,val2 = max(left[0], left[1]) + max(right[0], right[1]);
- 最后当前节点的状态就是{val2, val1}, 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}。
- 举例推导dp数组
class Solution {
// 状态标记递归
// 不偷:Max(左孩子不偷,左孩子偷) + Max(又孩子不偷,右孩子偷)
// root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) +
// Math.max(rob(root.right)[0], rob(root.right)[1])
// 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
// root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
public int rob(TreeNode root) {
int[] res = robAction(root);
return Math.max(res[0], res[1]);
}
int[] robAction(TreeNode root) {
int[] res = new int[2];
if (root == null) {
return res;
}
int[] left = robAction(root.left);
int[] right = robAction(root.right);
res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
res[1] = root.val + left[0] + right[0];
return res;
}
}