学习:动态规划(打家劫舍)
follow:代码随想录
198. 打家劫舍1⃣️
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
解析:
每偷窃到一个屋子,有两种选择:要么不偷该屋子,此时偷盗的金额为偷盗上一个屋子时的金额;要么偷该屋子,但是上一个屋子不能偷,此时偷盗的金额是偷盗上上个屋子的金额加上偷盗该屋子的金额。
所以构建dp数组:dp[i]:当有i个房屋时,能偷窃的最高金额
dp[i] = max(dp[i - 1], dp[i - 1] + nums[i])
初始化:
偷窃第一个房屋时,能偷窃的最高金额是该房屋的金额
偷窃第二个房屋时,能偷窃的最高金额是第一个房屋和第二个房屋的最高金额
解答:
public int rob(int[] nums) {
//dp[i]:当有i个房屋时,能偷窃的最高金额
int[] dp = new int[nums.length];
//初始化
dp[0] = nums[0];
if(nums.length == 1) return dp[0];
dp[1] = Math.max(nums[0], nums[1]);
//递推公式dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
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];
}
213. 打家劫舍2⃣️
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
解析:
这道题再上一题的基础上,增加一个围成一圈,也就是说不能同时偷窃第一个房屋和最后一个房屋。所以就有两种考虑方式:
- 考虑偷第一家,但是不能偷最后一家
- 考虑偷第二家,但是不能偷第一家
解答:
public int rob(int[] nums) {
//dp[i]:当有i个房屋时,能偷窃的最高金额
int[] dp = new int[nums.length];
//有两种方案:选第一个房屋但是不选最后一个房屋
dp[0] = nums[0];
if(nums.length == 1) return dp[0];
dp[1] = Math.max(nums[0], nums[1]);
if(nums.length == 2) return dp[1];
for(int i = 2; i < nums.length - 1; i++){
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
int temp = dp[nums.length - 2];
//第二种方法:选择最后一个房屋但是不选择第一个房屋
dp[1] = nums[1];
dp[2] = Math.max(nums[1], nums[2]);
for(int i = 3; i < nums.length; i++){
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return Math.max(temp, dp[nums.length - 1]);
}
337. 打家劫舍3⃣️
题目描述:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
解析:
递归法:
每遍历到一个节点,考虑两种情况
- 包括该节点,但不能抢左右孩子节点
- 不包括该节点,抢左右孩子节点
//记忆法递归
Map<TreeNode, Integer> map = new HashMap<>();
public int rob(TreeNode root) {
if(root == null) return 0;
if(root.left == null && root.right == null) return root.val;
if(map.containsKey(root)) return map.get(root);
int temp = root.val;
//包括该节点,不包括左右孩子
if(root.left != null) temp += rob(root.left.left) + rob(root.left.right);
if(root.right != null) temp += rob(root.right.left) + rob(root.right.right);
//不包括该节点,包括左右孩子
int temp2 = 0;
//这里重复计算了:父节点计算了孙子节点,孩子节点也计算了孙子节点
if(root.left != null) temp2 += rob(root.left);
if(root.right != null) temp2 += rob(root.right);
map.put(root, Math.max(temp, temp2));
return Math.max(temp, temp2);
}
动态规划法
这是一个树形动规题,需要同时考虑树的遍历方法 + 状态转移。
本题用的是后序遍历,状态转移是一个节点的最大值,根据左后孩子节点的最大值来计算,dp二维需要记录的时当前节点偷与不偷的最高金额。
- 确定递归函数的参数和返回值
参数是当前节点,返回值是状态二维数组 - 确定终止条件
遇到空节点时,终止,返回[0,0] - 确定单层逻辑
根据本题的要求,只有两种方法偷窃:一个是偷本节点,一个是不偷本节点,偷左右节点。
所有单层的逻辑,就是找到这两者的最大值。
先遍历左节点和右节点,根据返回的二维状态数组,计算最大值
public int rob(TreeNode root) {
int[] result = postOrder(root);
return Math.max(result[0], result[1]);
}
public int[] postOrder(TreeNode node){
if(node == null) return new int[]{0,0};
int[] left = postOrder(node.left);
int[] right = postOrder(node.right);
int[] record = new int[2];
//考虑左右孩子节点,但不一定选择左右孩子节点
record[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
record[1] = node.val + left[0] + right[0];
return record;
}