【代码训练营】day47 | 198.打家劫舍 & 213.打家劫舍II & 337.打家劫舍III

文章介绍了LeetCode中三道与打家劫舍相关的算法题,主要涉及动态规划思想。第一题是线性数组的打家劫舍问题,通过dp[i]表示考虑下标i及之前所能偷得的最大金额。第二题是环形数组的打家劫舍,通过排除首尾相连的情况转化为线性问题。第三题将问题扩展到树形结构,每个节点有两种状态(偷或不偷),采用后序遍历解决。
摘要由CSDN通过智能技术生成

所用代码 java

打家劫舍 LeetCode 198

题目链接:打家劫舍 LeetCode 198 - 中等

思路

当前的房间偷或者不偷,和前一个房间和前两个房间是有关系的。

  • dp[i]:考虑到下标i(包括i)之前的,所能偷的最大金额为dp[i]

  • 递推公式:dp[i] = max(dp[i-1], dp[i-2] + nums[i]);

    • 偷idp[i-2] + nums[i] => i-2及之前是我们考虑的范围
    • 不偷idp[i-1] => i-1及之前是我们考虑的范围
  • 初始化:dp[0]=nums[0], dp[1]=max(nums[0],nums[1]);

  • 遍历顺序:从小到大 2<=i<nums.size

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]、、、),是怎么推出来的,就可以很容易的实现状态转移方程。

打家劫舍II LeetCode 213

题目链接:打家劫舍II LeetCode 213 - 中等

思路

本题和打家劫舍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的扩展与理解。

打家劫舍III LeetCode 337

题目链接:打家劫舍III LeetCode 337 - 中等

思路

无。


本题是一个树形结构的入门级别dp。。。

每个结点有两个状态

  • 不偷:dp[0] 不偷当前结点所获得的最大金钱
  • 偷:dp[1] 偷当前结点所获得的最大金钱

即每一层递归里面都有长度为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;
    }
}

总的来说,就是看偷不偷根结点,然后往下去遍历!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值