打家劫舍类问题

文章介绍了几个基于动态规划的算法问题,包括打家劫舍I、II、III,以及买卖股票的最佳时机IV和包含冷冻期的问题。这些问题的核心在于根据状态转移方程进行决策,以获得最大收益。在打家劫舍问题中,涉及了是否盗窃相邻房屋的选择;打家劫舍II引入了环状设定;打家劫舍III结合了二叉树的后序遍历;买卖股票问题则考虑了交易次数限制和冷冻期的影响。每个问题都提供了详细的解题思路和代码实现。
摘要由CSDN通过智能技术生成

打家劫舍类问题

打家劫舍属于是很经典的动态规划类的问题了,基本思想就是分情况讨论,然后取最大值,我们要把情况想全。

198. 打家劫舍

本题属于是打家劫舍里比较基础的题目了。

当前情况只有两种,就是偷或者不偷,取决于上一家偷过没,上一家偷过了,当前就不偷,反之也成立。

  1. dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

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

  3. 初始化:从递推公式可以看出,递推公式的基础就是dp[0] 和 dp[1]。所以dp[0] 和 dp[1]都需要初始化为各自情况下的最大值

    dp[0] = nums[0];
    dp[1] = Math.max(nums[0],nums[1]);
  4. 遍历顺序:从前到后

class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) return nums[0];
        if (nums.length == 2) return Math.max(nums[0],nums[1]);
        //定义动态规划数组
        int[] dp = new int[nums.length];
        //初始化
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0],nums[1]);
        //有两种情况:1.前一家偷了 2.前一家没偷
        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. 打家劫舍 II

本题加了一个环上的设定,就会比较不知道从哪里下手。分情况讨论也不知道怎么分情况。

本题在环上的判定,我们要分三种情况。

  1. 去掉数组头和尾

  2. 去掉数组的尾

  3. 去掉数组的头

然后再这三种情况下分别求198. 打家劫舍的解,再返回一个最大值即可,其中我们情况一和情况二三重合了,所以我们只用考虑情况二三即可。

class Solution {
    public int rob(int[] nums) {
        int len = nums.length;
        if (len == 1) return nums[0];
        if (len == 2) return Math.max(nums[0], nums[1]);
        return Math.max(robAction(nums,0,len-1), robAction(nums, 1, len));
    }
    //左闭右开区间
    public int robAction(int[] nums, int start, int end) {
        int[] dp = new int[nums.length];
        dp[start] = nums[start];
        dp[start+1] = Math.max(nums[start], nums[start+1]);
        for (int i = start+2; i < end; i++) {
            dp[i] = Math.max(dp[i-2]+nums[i], dp[i-1]);
        }
        return dp[end-1];
    }
}

337. 打家劫舍 III

本题属于和二叉树结合起来的问题了,相对来说是比较难的。需要对二叉树的遍历比较熟悉才可以。

首先我们需要确定遍历顺序,本题应该是后序遍历。在确定当前节点的最大值的时候,需要根据其左右孩子来得出结果。

所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

这里的dp数组和之前做过的所有动态规划的题的定义都不一样了,不是题目问什么,我们定义什么了。

本题因为是要遍历二叉树,所以要递归,递归三部曲

  1. 确定递归函数的参数和返回值

    这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。参数为当前节点

    public int[] robActive(TreeNode root) {}

  2. 确定终止条件

    在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

    if (root == null) return dp;
  3. 确定遍历顺序

    前面我们已经说了,应该是后序遍历。这里我们主要来理一下单层递归的逻辑。

    如果是偷当前节点,那么左右孩子就不能偷;如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的

    //用后序遍历
    int[] left = robActive(root.left);//左
    int[] right = robActive(root.right);//右
    //中
    dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);//当前节点不偷
    dp[1] = root.val + left[0] + right[0];//当前节点偷

整体代码

class Solution {
    public int rob(TreeNode root) {
        int[] dp = robActive(root);
        return Math.max(dp[0], dp[1]);
    }
    public int[] robActive(TreeNode root) {
        int[] dp = new int[2];//dp[0]:表示当前节点不偷的盗取最高金额 dp[1]:表示当前节点偷的盗取最高金额
        if (root == null) return dp;
        //用后序遍历
        int[] left = robActive(root.left);
        int[] right = robActive(root.right);
        //中
        dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        dp[1] = root.val + left[0] + right[0];
        return dp;
    }
}

188. 买卖股票的最佳时机 IV

在上一题的基础上,本题又将能卖多少次进行了变量上的限定,那我们是不是要在每个循环中列出k个等式给dp数组赋值呢?

其实不用,我们观察上一题的递推公式。

dp[i][1] = Math.max(dp[i-1][1],-prices[i]);//第i天,第一次持有
dp[i][2] = Math.max(dp[i-1][2],dp[i-1][1] + prices[i]);//第i天,第一次不持有
dp[i][3] = Math.max(dp[i-1][3],dp[i-1][2] - prices[i]);//第i天,第二次持有
dp[i][4] = Math.max(dp[i-1][4],dp[i-1][3] + prices[i]);//第i天,第二次不持有

发现,两个赋值操作为一组,其实组与组之间是一样的。

这样我们就能用第二个维度来控制k了。

class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) return 0;
​
        // [天数][股票状态]
        // 股票状态: 奇数表示第 k 次交易持有/买入, 偶数表示第 k 次交易不持有/卖出, 0 表示没有操作
        int len = prices.length;
        int[][] dp = new int[len][k*2 + 1];
        
        // dp数组的初始化, ,只赋值买入状态
        for (int i = 1; i < k*2; i += 2) {
            dp[0][i] = -prices[0];
        }
​
        for (int i = 1; i < len; i++) {
            for (int j = 0; j < k*2 - 1; j += 2) {
                dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);//当天有 = 前一天有 或者 前一天没有-当天买
                dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);//当天没有 = 前一天没有 或者 当天有+当天卖
            }
        }
        return dp[len - 1][k*2];
    }
}

309. 最佳买卖股票时机含冷冻期

本题的情况就更复杂了些,因为有冷冻期的存在,所以讨论起来很麻烦,很容易想着想着就乱了,这里给出一个比较好理解的版本

总体上分为三大类,今天持有股票,今天不持有股票没在冷冻期,今天不持有股票且在冷冻期。

而今天不持有股票又分为两类:今天才把股票卖了,今天以前就把股票卖了。

大致是这个样子的结构

  • 今天持有股票(状态1)

  • 今天不持有股票没在冷冻期

    • 今天及以前就把股票卖了(状态2)

    • 今天才把股票卖了(状态3)

  • 今天在冷冻期。(状态4)

我们分别把他们定义到dp数组中:dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]

我们分情况讨论

  1. 今天持有股票(状态1):可能造成今天持有股票的情况:

    • 昨天就持有股票:dp[i - 1][0]

    • 昨天是冷静期,今天买入股票:dp[i - 1][3] - prices[i]

    • 昨天及以前都不持有股票,今天买入股票:dp[i - 1][1] - prices[i])

    所以状态1的递推公式为:dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));

  2. 今天及以前就把股票卖了(状态2):

    • 昨天就是这样的状态(不是冷冻期且不持有股票):dp[i - 1][1]

    • 昨天是冷冻期:dp[i - 1][3]

  3. 今天才把股票卖了(状态3):

    • 只有一种可能,昨天是持有股票状态的:dp[i - 1][0] + prices[i];

  4. 今天在冷冻期。(状态4)

    • 只有一种可能,昨天把票卖了:dp[i - 1][2]

初始化

根据递推公式和dp数组的定义:应把dp[0][0] = -prices[0],dp[0][1],dp[0][2],dp[0][3]定义为0。

遍历顺序

从递归公式上可以看出,dp[i] 依赖于 dp[i-1],遍历顺序应该是从前往后。

举例推导

最后结果是取 状态二,状态三,和状态四的最大值,很容易把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。

class Solution {
   public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][4];
        //初始化
        dp[0][0] = -prices[0];
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }
        return Math.max(dp[prices.length-1][1], Math.max(dp[prices.length-1][2], dp[prices.length-1][3]));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值