打家劫舍类问题
打家劫舍属于是很经典的动态规划类的问题了,基本思想就是分情况讨论,然后取最大值,我们要把情况想全。
本题属于是打家劫舍里比较基础的题目了。
当前情况只有两种,就是偷或者不偷,取决于上一家偷过没,上一家偷过了,当前就不偷,反之也成立。
-
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
-
递推公式:
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
-
初始化:从递推公式可以看出,递推公式的基础就是dp[0] 和 dp[1]。所以dp[0] 和 dp[1]都需要初始化为各自情况下的最大值
dp[0] = nums[0]; dp[1] = Math.max(nums[0],nums[1]);
-
遍历顺序:从前到后
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]; } }
本题加了一个环上的设定,就会比较不知道从哪里下手。分情况讨论也不知道怎么分情况。
本题在环上的判定,我们要分三种情况。
-
去掉数组头和尾
-
去掉数组的尾
-
去掉数组的头
然后再这三种情况下分别求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]; } }
本题属于和二叉树结合起来的问题了,相对来说是比较难的。需要对二叉树的遍历比较熟悉才可以。
首先我们需要确定遍历顺序,本题应该是后序遍历。在确定当前节点的最大值的时候,需要根据其左右孩子来得出结果。
所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
这里的dp数组和之前做过的所有动态规划的题的定义都不一样了,不是题目问什么,我们定义什么了。
本题因为是要遍历二叉树,所以要递归,递归三部曲
-
确定递归函数的参数和返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。参数为当前节点
public int[] robActive(TreeNode root) {}
-
确定终止条件
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
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];//当前节点偷
整体代码
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; } }
在上一题的基础上,本题又将能卖多少次进行了变量上的限定,那我们是不是要在每个循环中列出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]; } }
本题的情况就更复杂了些,因为有冷冻期的存在,所以讨论起来很麻烦,很容易想着想着就乱了,这里给出一个比较好理解的版本
总体上分为三大类,今天持有股票,今天不持有股票没在冷冻期,今天不持有股票且在冷冻期。
而今天不持有股票又分为两类:今天才把股票卖了,今天以前就把股票卖了。
大致是这个样子的结构
-
今天持有股票(状态1)
-
今天不持有股票没在冷冻期
-
今天及以前就把股票卖了(状态2)
-
今天才把股票卖了(状态3)
-
-
今天在冷冻期。(状态4)
我们分别把他们定义到dp数组中:dp[i][j]
,第i天状态为j,所剩的最多现金为dp[i][j]
。
我们分情况讨论
-
今天持有股票(状态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):
-
昨天就是这样的状态(不是冷冻期且不持有股票):
dp[i - 1][1]
-
昨天是冷冻期:
dp[i - 1][3]
-
-
今天才把股票卖了(状态3):
-
只有一种可能,昨天是持有股票状态的:
dp[i - 1][0] + prices[i];
-
-
今天在冷冻期。(状态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])); } }