一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。
示例 3:
输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
直接递归:
这种dfs穷举法的时间复杂度是指数级的,对于长度范围为100的测试用例是肯定会超TLE的。
class Solution {
public int rob(int[] nums) {
return dfs(nums, 0, false);
}
/* 返回第i个房屋及之后能偷窃到的最大值,issteal标志第i - 1个房屋是否偷窃 */
int dfs(int[] nums, int i, boolean issteal) {
/* i越界,没房屋可供偷窃,只能空手而归了>_< */
if (i >= nums.length)
return 0;
/* 前一个偷过了,那这个就不能偷了!去下一家看看~ */
else if (issteal)
return dfs(nums, i + 1, false);
/* 前一个没偷过,那么这个可偷可不偷,两者取大的那个 */
else
return Math.max(dfs(nums, i + 1, true) + nums[i], dfs(nums, i + 1, false));
}
};
记忆化递归:
class Solution {
//map表示从第start个房子开始,偷到最后一个房子的最大值
Map<Integer,Integer> map=new HashMap<>();
public int rob(int[] nums) {
if(nums.length==0)
return 0;
return dfs(nums,0);
}
public int dfs(int[] nums,int start)
{
if(start>=nums.length)
return 0;
//查表
if(map.containsKey(start))
return map.get(start);
//分两种情况
//1.偷第start个房子,然后只能从第start+2个房子开始偷
int hasFirst=nums[start];
hasFirst+=dfs(nums,start+2);
//2.不偷第start个房子,从第start+1个房子偷
int noFirst=0;
noFirst+=dfs(nums,start+1);
//取两种情况的最大值
int ans=Math.max(hasFirst,noFirst);
//把从第start个房子开始,偷到最后一个房子的最大值ans记录下来
map.put(start,ans);
return ans;
}
}
其实,这个就是转移方程
int ans=Math.max(hasFirst,noFirst);
只需要增加一个数组把计算过之后的记录保存下来,在每一次DFS进入时候判断这个值是否被计算过,就是动态规划了。
动态规划并没有优化时间复杂度,它的优化就是通过保存之前的计算过程把树形计算剪枝成线性计算。
本题既有爬楼梯的影子,也有最长连续子序列和的影子。
解答本题的关键,在于理解题目表述中,预约服务要有休息时间的含义。其意思不仅在于,不能接收相邻的预约这么简单。结合要找预约时间最长,我们要能够分析出,最优预约集合中的最后一个元素,不是nums[nums.size()-1] 就是 nums[nums.size()-2]
举个例子,对于任意预约序列[a0,a1,a2,a3,…,an-3,an-2,an-1]来说,其符合题目要求的最优预约集合中,最后一个元素一定出自 an-2 或 an-1。如果可以理解到这个层面,本题就可以成功转化为一个普通的一维动态规划问题了。即我们的目标是,找到以an-2 和 an-1 结尾的各自最大预约,然后比较他俩谁更大,即可。更进一步,要找an-2结尾的各自最大预约,就是要找an-4 和 an-5的最大预约(因为不能接收相邻预约嘛,所以要隔着找)
由此,就可以清楚了解到其子问题。
思路解释:
(1)base case
基础问题比较简单,因为其不接受相邻预约,所以基础问题就是只有1个预约时,以这个预约结尾的最优预约时间就是:
dp[0] = nums[0];
如果有两个预约,以后一个预约结尾的最优预约时间就是:
dp[1] = nums[1];
(2)确定状态
状态就是子问题中变化的量,经过上面的分析,子问题n就是,以采纳n号元素作为最优预约结尾的最优预约时长。
所以说,这个状态就是以 第几个预约作为最优预约的结尾元素。
(3)确定选择
选择就是导致状态改变的原因,对于子问题i,我们一定要把nums[i]加入到当前子问题的最优预约中,所以为达成这个子问题目的,
选择就是,从前面i-2个子问题中(要排除掉第i-1个,因为不接受相邻),选择预约时长最长的组合加进来
(4)确定dp数组含义
其实这个确定含义的工作,在题目理解的部分已经做过了。
dp[i]的含义就是,对于nums的前i+1个元素组成的子预约集合中,以nums[i]作为最优预约集合结尾元素时,最优预约时间长度。
这么说不是很好理解,举个例子。
对于数组[2,7,9,3,1]来说,其dp[3] 表示 在[2,7,9,3]这个子数组中,以3这个元素作为最优预约集合结尾元素时,其最优预约的时间长度。
以3结尾时,最优预约集合为[7,3],所以dp[3] = 10。
同理,dp[4],就是表示在[2,7,9,3,1]这个子数组中,以1这个元素作为最优预约集合结尾元素时,其最优预约的时间长度。
以1结尾时,最优预约集合为[2,9,1],所以dp[4] = 12.
(5)写状态转移方程
清楚的理解了每一步后,状态转移方程就很好写了。
首先dp[0] = nums[0]
dp[1] = nums[1]
对于i >= 2来说, dp[i] = max(dp[0],dp[1],dp[2],…,dp[i-3],dp[i-2]) + nums[i];
同理,上边的方程中没有dp[i-1] 是因为不能取相邻。
相信到这里,大家都很容易理解这个方程的含义了。
同时,我们不需要额外的循环去实现这个max,我们只需要通过一个变量来保存即可。
class Solution {
public int massage(int[] nums) {
if(nums.length==0) //特殊情况
return 0;
else if(nums.length==1){ //特殊情况
return nums[0];
}
else{
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
int max = dp[1]; **//max用于记录过往的最大值
for(int i=2;i<nums.length;i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]); // 上一个dp[i-1]最大值与前一个dp[i-2]+nums[i]相比较,避免相邻情况。
max = Math.max(max,dp[i]);
}
return max;
}
}
}
优化:
一般的动态规划 。 本来应该是定义dp数组的。但是在这里 n位置的结果,只需要n-1和 n-2的结果就可以推出来,所以只需要保存两个结果。first second的作用和dp数组一样。
class Solution {
public int massage(int[] nums) {
int first=0;
int second=0;
for(int i:nums){
int temp=second;
second=Math.max(second,first+i);
first=temp;
}
return second;
}
}
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int len = nums.length;
if (len == 1)
return nums[0];
return Math.max(dfs1(nums, 0, false), dfs2(nums, 1, false));
}
//比较0 -> len-1 和 1 -> len 之间哪个更大
int dfs1(int[] nums, int i, boolean issteal) {
if (i >= nums.length - 1)
return 0;
else if (issteal)
return dfs1(nums, i + 1, false);
else
return Math.max(dfs1(nums, i + 1, true) + nums[i], dfs1(nums, i + 1, false));
}
int dfs2(int[] nums, int i, boolean issteal) {
if (i >= nums.length)
return 0;
else if (issteal)
return dfs2(nums, i + 1, false);
else
return Math.max(dfs2(nums, i + 1, true) + nums[i], dfs2(nums, i + 1, false));
}
}
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int len = nums.length;
if (len == 1)
return nums[0];
return Math.max(robAction(nums, 0, len - 1), robAction(nums, 1, len));
}
int robAction(int[] nums, int start, int end) {
int pre = 0, cur = 0, tmp = 0;
for (int i = start; i < end; i++) {
tmp = cur;
cur = Math.max(cur, pre + nums[i]);
pre = tmp;
}
return cur;
}
}
爷爷节点获取到最大的偷取的钱数呢
根据以上条件,我们可以得出单个节点的钱该怎么算
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);
}
//四个孙子+爷爷 vs 两个儿子
//计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。
//这里也计算了 两个儿子的情况,所以存在重复计算
return Math.max(money, rob(root.left) + rob(root.right));
}
}
class Solution {
public int rob(TreeNode root) {
Map<TreeNode, Integer> memo = new HashMap<>();
return robAction(root, memo);
}
public int robAction(TreeNode root, Map<TreeNode, Integer> memo) {
if (root == null)
return 0;
if (memo.containsKey(root)) //如果该节点已计算过,直接返回
return memo.get(root);
int money = root.val;
if (root.left != null) {
money += robAction(root.left.left, memo) + robAction(root.left.right, memo);
}
if (root.right != null) {
money += robAction(root.right.left, memo) + robAction(root.right.right, memo);
}
int res = Math.max(money, robAction(root.left, memo) + robAction(root.right, memo));
memo.put(root, res);
return res;
}
}
每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷
我们使用一个大小为 2 的数组来表示 int[] dp= new int[2] 0 代表不偷,1 代表偷
任何一个节点能偷到的最大钱的状态可以定义为
class Solution {
public int rob(TreeNode root) {
int[] res = robAction1(root);
return Math.max(res[0], res[1]); //选择是否偷当前节点的最大值
}
public int[] robAction1(TreeNode root) {
int res[] = new int[2];
if (root == null)
return res;
int[] left = robAction1(root.left);
int[] right = robAction1(root.right);
//不偷当前节点 与 偷当前节点
res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); //当前节点选择不偷 = 左孩子能偷到的钱 + 右孩子能偷到的钱
res[1] = root.val + left[0] + right[0]; //当前节点选择偷 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
return res;
}
}