题目列表
逐一攻破
198、打家劫舍(中等)
看到题目,我们以常规动态规划的思路去思考:
- 状态:题目有哪些变量?第i间房子的金钱数,每次能够偷盗的房间不允许相邻。目前小偷的位置
- 于是我们描述出:dp[i]表示偷盗到第i家房子时的最大金额数
- 思考递推过程,目前的位置在第i家房子,不允许相邻偷盗,则在我们之前的房子中选择能够偷盗的最大金钱max的房子,dp[i] = max+nums[i-1];i-1为第i家房子的金钱。
- 返回结果:返回所有dp[i]中的最大值;
- base case:dp[0]表示没有房子,则dp[0]=0;dp[1]肯定是第一家房子能偷,则dp[1] = nums[0];
得到如下代码:
class Solution {
public int rob(int[] nums) {
if(nums==null || nums.length<=0) return 0;
int[]dp = new int[nums.length+1];//dp[i]表示目前已经偷盗到第i家房子的最大金额
//base case :dp[0] = 0,dp[1] = nums[0];//第一间房子的价格
dp[1] = nums[0];
for(int i=2;i<dp.length;i++){//偷盗到第i家房子
int max = 0;
for(int j=i-2;j>=0;j--){
//相邻房间不能偷,找到之前的最大价格的dp[j];
if(max<dp[j]){
max = dp[j];
}
}
dp[i] = max + nums[i-1];
}
//返回所有的dp[i]中的最大价格
int max = 0;
for(int n=dp.length-1;n>=0;n--){
if(max<dp[n]){
max = dp[n];
}
}
return max;
}
}
优化:
class Solution {
public int rob(int[] nums) {
if(nums==null || nums.length<=0) return 0;
int[]dp = new int[nums.length+1];//dp[i]表示目前已经偷盗到第i家房子的最大金额
//base case :dp[0] = 0,dp[1] = nums[0];//第一间房子的价格
dp[1] = nums[0];
int max_now=nums[0];
for(int i=2;i<dp.length;i++){//偷盗到第i家房子
int max = 0;
for(int j=i-2;j>=0;j--){
//相邻房间不能偷,找到之前的最大价格的dp[j];
if(max<dp[j]){
max = dp[j];
}
}
dp[i] = max + nums[i-1];
if(max_now<dp[i]){
max_now = dp[i];
}
}
return max_now;
}
}
100%不应该,这里复杂度为时间复杂度O(n2)
我们的思路是找到能偷的最大金钱,但是复杂度却为O(N2),应该可化简。我们尝试修改一下dp数组的定义:
之前:dp[i]表示偷盗到第i间房子得到的最大价格(默认是偷了第i家),我们思考递推的时候是从前i-2间房子中找到最大的偷盗价格,使得我们多了一层遍历。
现在
换个描述,就是dp[i]表示前i个房子能够偷盗到的最大金额,
- 则我可以不偷第i家,因为我上一次可能偷的是第i-1家,再偷就报警了。
- 我可以偷第i家,因为我上次偷的是前i-2之前的某一家。
dp[i] = max(dp[i-2]+nums[i-1],dp[i-1]);
base case:dp[0] = 0;第0家房子没得;dp[1] = nums[0];//第一家房子
class Solution {
public int rob(int[] nums) {
if(nums==null || nums.length<=0) return 0;
int[]dp = new int[nums.length+1];//dp[i]表示目前已经偷盗到第i家房子的最大金额
//base case :dp[0] = 0,dp[1] = nums[0];//第一间房子的价格
dp[1] = nums[0];//
for(int i=2;i<dp.length;i++){//偷盗到第i家房子
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i-1]);
}
return dp[nums.length];
}
}
时间复杂度O(n),空间复杂度为O(n),也可以使用滚动数组优化:
class Solution {
public int rob(int[] nums) {
if(nums==null || nums.length<=0) return 0;
int temp_1 = nums[0];
int temp_0 = 0;
int max = temp_1;
for(int i=2;i<nums.length+1;i++){//偷盗到第i家房子
max = Math.max(temp_0+nums[i-1],temp_1);
temp_0 = temp_1;
temp_1 = max;
}
return max;
}
}
213、打家劫舍II(中等)
添加了环形房间的限制,但是我们必然只能从头偷盗尾,不可能还能绕回来偷(利益最大化的情况下)。我们可以通过割裂问题的方法来做。然后我们又回到了问题1了。
class Solution {
//第一间和最后一间不能同时被抢,那我就可以把这些房间割裂成两个问题来看
//第一间到倒数第二间
//第二间到最后一间
//分别计算出其最大的价格,然后选取最大的值即可。
public int rob(int[] nums) {
if(nums==null || nums.length<=0) return 0;
if(nums.length==1) return nums[0];//只有一间直接返回
int[]dp1 = new int[nums.length+1];//dp[i]表示目前已经偷盗到第i家房子的最大金额
int[]dp2 = new int[nums.length+1];
//base case :dp[0] = 0,dp[1] = nums[0];//第一间房子的价格
//割裂问题1,第一间到倒数第二间
dp1[1] = nums[0];//也就是这里的i和num中的是i-1对应
for(int i=2;i<dp1.length-1;i++){//偷盗到第i家房子
dp1[i] = Math.max(dp1[i-1],dp1[i-2]+nums[i-1]);
}
//
dp2[1] = nums[1];//这里的i对应num里面的i
for(int i=2;i<dp2.length-1;i++){
dp2[i] = Math.max(dp2[i-1],dp2[i-2]+nums[i]);
}
return Math.max(dp2[nums.length-1],dp1[nums.length-1]);
//割裂问题2:第二间到最后一间
}
}
337、打家劫舍III(中等)
这里变到了树的结构,而既然出现了树,我们就要按照做树系列的题目思维来思考:
- 根结点要做什么?
- 每个结点要做什么?
- 左孩子和右孩子哪个先遍历
约束:直接相连的不允许访问,所以我们要根据选择去做。感觉这道题比之前的比较来说,容易想一点:
class Solution {
Map<TreeNode,Integer> memo = new HashMap<>();
public int rob(TreeNode root) {
if(root==null) return 0;
if(memo.containsKey(root)) return memo.get(root);
int temp = root.val;//去除当前的值
int left1 = 0,left=0;//left1表示不含left结点的最大价值
int right1=0,right=0;
//抢了root,则不能抢其left、right,而是root.left.left/right和root.right.left/right
left = root.left==null?0:rob(root.left.left)+rob(root.left.right);
right = root.right==null?0:rob(root.right.left)+rob(root.right.right);
//偷了root,则直接相连的不能访问
int robNow1 = temp+left+right;
//没偷root,则可以访问
int robNow2 = rob(root.left)+rob(root.right);
int res = Math.max(robNow2,robNow1);//偷,则返回res,不偷则返回left+right;
memo.put(root,res);
return res;
}
}