所用代码 java
当前的房间偷或者不偷,和前一个房间和前两个房间是有关系的。
dp[i]:考虑到下标i(包括i)之前的,所能偷的最大金额为dp[i]
递推公式:dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
dp[i-2] + nums[i]
=> i-2及之前是我们考虑的范围dp[i-1]
=> i-1及之前是我们考虑的范围初始化:dp[0]=nums[0], dp[1]=max(nums[0],nums[1]);
遍历顺序:从小到大 2<=i
class Solution {
public int rob(int[] nums) {
// 保证nums往下传至少有2个数
if (nums.length == 0 || nums == null) 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]);
// 从i=2开始从小到大遍历
for (int i = 2; i < nums.length; i++) {
// dp[i]的状态取决于是否偷i
// 偷i:那i-1就没法偷,就取决于i-2能偷多少,再加上nums[i]
// 不偷i:那i能偷多少由i-1决定
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[nums.length-1];
}
}
第一次做这题确实不会,主要是递推公式没想出来。
我们在考虑某个值dp[j]的时候,就要想到该值怎样由前面的状态推出来(dp[i-1]、dp[i-2]、、、),是怎么推出来的,就可以很容易的实现状态转移方程。
本题和打家劫舍1的主要区别是首尾不能相连,就是说这是一个环形的数组,那要怎样才能处理环形的数组呢,当然是化环形为普通链表。
样例 | 1 9 1 6 1 |
---|---|
情况1 | 只取中间 9 1 6 |
情况2 | 要头不要尾 1 9 1 6 |
情况3 | 要尾不要头 9 1 6 1 |
其实情况2和情况3是包含了情况1的,我们的dp数组的含义是考虑到i所能偷的最大金额是dp[i],所以i可有可无,这就是为什么情况2和情况3包含情况1的原因,我们可以有头,也可以没有头,尾部也一样。
通过这三种情况我们就可以化圆为链,把复杂的问题简单化,再通过打家劫舍1的方法,比较得出最优解。
class Solution {
public int rob(int[] nums) {
// 保证nums往下传至少有2个数
if (nums.length == 0 || nums == null) return 0;
if (nums.length == 1) return nums[0];
int result1 = rob1(nums, 0, nums.length-1);
int result2 = rob1(nums, 1, nums.length);
return result1 > result2 ? result1 : result2;
}
// 左闭右开
public int rob1(int[] nums, int start, int end) {
if (end - start == 1) return nums[start];
int[] dp = new int[nums.length];
// 初始化
dp[start] = nums[start];
dp[start + 1] = Math.max(nums[start],nums[start + 1]);
// 从i=2开始从小到大遍历
for (int i = 2 + start; i < end; i++) {
// dp[i]的状态取决于是否偷i
// 偷i:那i-1就没法偷,就取决于i-2能偷多少,再加上nums[i]
// 不偷i:那i能偷多少由i-1决定
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[end-1];
}
}
通过一个思想,把首尾有关联的状态给分开,拆分成这种无关联状态,是2的思想,本质在于对1的扩展与理解。
无。
本题是一个树形结构的入门级别dp。。。
每个结点有两个状态
即每一层递归里面都有长度为2记录偷与不偷的dp数组,当前成的dp数组就表示当前层的结点状态。
后序遍历:因为我们要使用后面递归遍历的参数,然后再后面进行一个运算。
class Solution {
public int rob(TreeNode root) {
int[] value = traveral(root);
// 返回根结点 偷或不偷 的最大值
return Math.max(value[0], value[1]);
}
public int[] traveral(TreeNode root){
if (root == null) return new int[]{0,0};
int[] leftDp = traveral(root.left); // 左
int[] rightDp = traveral(root.right); // 右
// 中
// 1、dp[0],不偷,左右就可以偷,也可以不偷
// max(左子树偷或不偷) + max(右子树偷或不偷)
int valueDo = Math.max(leftDp[0],leftDp[1]) + Math.max(rightDp[0],rightDp[1]);
// 2、dp[1],偷,左右就不能偷
// root.val + 左子树不偷 + 右子树不偷
int valueNot = root.val + leftDp[0] + rightDp[0];
// 返回该结点 偷或不偷 获得的最大金币
return new int[]{valueDo, valueNot};
}
}
本题非常的巧妙,把dp的想法运用到的二叉树上面,把每个结点看成dp的两种状态,每次返回的就是每种状态的最优解。
此外还可以直接暴力回溯:但是很遗憾超时了~
class Solution {
public int rob(TreeNode root) {
if (root == null)
return 0;
int money = root.val;
// 左孩子不为空,就去左孩子递归累加,每隔一个结点累加值
if (root.left != null) {
money += rob(root.left.left) + rob(root.left.right);
}
// 右孩子不为空,忘右去递归累加,同样隔一个结点累加
if (root.right != null) {
money += rob(root.right.left) + rob(root.right.right);
}
// 即以上为要偷根结点的情况
// rob(root.left) + rob(root.right) 为不偷根结点的情况,就往下去累加
return Math.max(money, rob(root.left) + rob(root.right));
}
}
但是我们可以对回溯进行优化,使用记忆化递归的方式(或者说备忘录方式),把每次递归的结点和结果记录下来,下次再遍历到该结点的时候就不用继续往下遍历了,相当于剪枝操作了。
关键代码就多了两行:这样就过了!
// 查看记录里面有没有
if (map.containsKey(root)) return map.get(root);
// 记录下每个结点值
map.put(root, res);
class Solution {
public int rob(TreeNode root) {
Map<TreeNode, Integer> map = new HashMap<>();
return traveral(root, map);
}
public int traveral(TreeNode root, Map<TreeNode, Integer> map){
if (root == null)
return 0;
// 如果该结点遍历过了,就不用再再遍历一次了
// 这就是记忆化的思路,或者说备忘录
if (map.containsKey(root)) return map.get(root);
int money = root.val;
if (root.left != null) {
money += traveral(root.left.left, map) + traveral(root.left.right, map);
}
if (root.right != null) {
money += traveral(root.right.left, map) + traveral(root.right.right, map);
}
int res = Math.max(money, traveral(root.left, map) + traveral(root.right, map));
// 每次递归后把该结点记录下来
map.put(root, res);
return res;
}
}
总的来说,就是看偷不偷根结点,然后往下去遍历!