【198. 打家劫舍】中等题(偏简单)
步骤:
1、确定dp[i]的含义
dp[i]:从0到i间房子中一夜之内能偷窃到的最高金额
2、确定递推关系
只有两种选择:偷 or 不偷 ?
偷:dp[i] = dp[i-2] + nums[i]
不偷:dp[i] = dp[i-1] (最高金额还是和从0到i-1间房子中一夜之内能偷窃到的最高金额一致)3、确定初始值
dp[0] = nums[0]; // 只有一间房,那肯定偷
dp[1] = Math.max(nums[0], nums[1]); // 有两间房(必定相邻),只能选其中最高金额的偷
class Solution {
public int rob(int[] nums) {
// dp[i]:从0到i间房子中一夜之内能偷窃到的最高金额
// 偷:dp[i] = dp[i-2] + nums[i]
// 不偷:dp[i] = dp[i-1] (最高金额还是和从0到i-1间房子中一夜之内能偷窃到的最高金额一致)
if (nums.length < 2) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0]; // 只有一间房,那肯定偷
dp[1] = Math.max(nums[0], nums[1]); // 有两间房(必定相邻),只能选其中最高金额的偷
for (int i = 2; i < dp.length; i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[nums.length-1];
}
}
- 时间复杂度:O(n),n是nums数组的长度,for循环遍历了一次
- 空间复杂度:O(n),dp数组的长度等于nums数组的长度
【优化】 空间复杂度O(n) -> O(1)
思路:最后返回的答案是数组的最后一个值,dp[i]只依赖于dp[i-1]和dp[i-2],所以其实可以不用额外的数组空间进行存取,使用三个变量,分别记录dp[i]、dp[i-1]和dp[i-2]即可。
易错点:最后的返回值使用y还是z?当数组中只有两个元素时,不进入for循环,此时y就是答案,所以最后的返回值不能是z,只能是y。
class Solution {
public int rob(int[] nums) {
// z:从0到i间房子中一夜之内能偷窃到的最高金额
// 偷:z = x + nums[i]
// 不偷:z = y (最高金额还是和从0到i-1间房子中一夜之内能偷窃到的最高金额一致)
if (nums.length < 2) return nums[0];
int x = nums[0];
int y = Math.max(nums[0], nums[1]);
int z = 0;
for (int i = 2; i < nums.length; i++){
z = Math.max(y, x + nums[i]);
x = y;
y = z;
}
return y;
}
}
- 时间复杂度:O(n),n是nums数组的长度,for循环遍历了一次
- 空间复杂度:O(1),无论输入的nums数组多长,都只用三个变量就足够
【213. 打家劫舍 II】中等题(思路要理清)
思路:
与【98.打家劫舍】的区别在于,头元素和尾元素存在互斥关系。(即偷了头元素,尾元素不能再偷;偷了尾元素,头元素不能再偷)取两种情况的最大值,即最高金额。
- 如果不偷头元素,那么尾元素才能考虑偷或不偷。
- 如果不偷尾元素,那么头元素才能考虑偷或不偷
步骤:
1、定义打家劫舍的函数(根据索引求nums某区间内的最高金额)
2、分别获取不偷头元素和不偷尾元素情况下的最高金额
3、取最大值作为最后的最高金额
class Solution {
public int rob(int[] nums) {
if (nums.length < 2) return nums[0];
int dp1 = robSimple(nums, 0, nums.length-2); // 不偷尾元素
int dp2 = robSimple(nums, 1, nums.length-1); // 不偷头元素
return Math.max(dp1, dp2);
}
// 根据索引求nums某区间内的最高金额
public int robSimple(int[] nums, int start, int end){
if (end == start) return nums[start];
int x = nums[start]; // 只有一间房,那肯定偷
int y = Math.max(nums[start], nums[start+1]); // 有两间房(必定相邻),只能选其中最高金额的偷
for (int i = start+2; i < end+1; i++){
int z = Math.max(y, x + nums[i]);
x = y;
y = z;
}
return y;
}
}
- 时间复杂度:O(n),n是nums数组的长度
- 空间复杂度:O(1),无论输入的nums数组多长,都只用三个变量就足够
【337. 打家劫舍 III】中等题(按照198题的思路不难)
思路:
肯定是要后序遍历,即左右中,综合考虑每条路的最高金额。
步骤:
1、确定输入参数和返回值
输入根节点root,返回值就是从根节点及根节点的所有子节点构成的二叉树偷窃的最高金额
关键:由于只能偷不相邻的房子,所以还要用额外的变量存储子节点的和作为间接的返回值,即使用全局变量sons记忆当前节点的左右儿子的最高金额之和,sons = leftSon + rightSon。
2、确定终止条件
如果当前节点为null,则当前节点的所有儿子的最高金额之和sons=0,当前节点的最高金额也为0。
3、确定单层递归逻辑
左右中的顺序,将当前节点看作爷爷,先获取左儿子和右儿子的最高金额,同时统计儿子的最高金额和、孙子的最高金额和,计算偷与不偷的最大值作为当前节点的最高金额。
- 不偷:偷所有儿子的
- 偷:偷爷爷自己的 + 偷所有孙子的
class Solution {
int sons = 0;
public int rob(TreeNode root) {
return robDfs(root);
}
// 递归 - 后序遍历(左右中)
public int robDfs(TreeNode root){
// 递归的终止条件
if (root == null) {
sons = 0; // 空节点的所有孩子最高金额的和为0
return 0; // 空节点的最高金额也为0
}
// 规则:
// -不偷:偷所有儿子的 = 最高金额选择1
// -偷:偷自己的 + 所有孙子的 = 最高金额选择2
int grandSons = 0;
int leftSon = robDfs(root.left); // 获取左儿子的最高金额
grandSons += sons; // 左儿子的儿子是孙子的一部分
int rightSon = robDfs(root.right); // 获取右儿子的最高金额
grandSons += sons; // 右儿子的而是是孙子的另一部分
sons = leftSon + rightSon; // 左儿子的最高金额 + 右儿子的最高金额 = 所有儿子的最高金额的和
return Math.max(sons, grandSons + root.val); // 选偷和不偷的最高金额
}
}
- 时间复杂度:O(n),n是所有节点的个数,递归遍历所有节点一次
- 空间复杂度:O(log n),递归栈所占的空间=二叉树的深度