打家劫舍
198. House Robber
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums
representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Example 1:
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.
Example 2:
Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
Total amount you can rob = 2 + 9 + 1 = 12.
Constraints:
1 <= nums.length <= 100
0 <= nums[i] <= 400
思路
大家如果刚接触这样的题目,会有点困惑,当前的状态我是偷还是不偷呢?
仔细一想,当前房屋偷与不偷取决于前一个房屋和前两个房屋是否被偷了。
所以这里就更感觉到,当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。
当然以上是大概思路,打家劫舍是dp解决的经典问题,接下来我们来动规五部曲分析如下:
- 确定dp数组(dp table)以及下标的含义
dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
- 确定递推公式
决定dp[i]的因素就是第i房间偷还是不偷。
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i]
,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
如果不偷第i房间,那么dp[i] = dp[i - 1]
,即考虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点)
然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
- dp数组如何初始化
从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
可以看出,递推公式的基础就是dp[0] 和 dp[1]
从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
代码如下:
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
- 确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
代码如下:
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
- 举例推导dp数组
以示例二,输入[2,7,9,3,1]为例。
红框dp[nums.size() - 1]
为结果。
C++解法
以上分析完毕,C++代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
Java解法
// 动态规划
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(dp[0], nums[1]);
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];
}
}
// 使用滚动数组思想,优化空间
// 分析本题可以发现,所求结果仅依赖于前两种状态,此时可以使用滚动数组思想将空间复杂度降低为3个空间
class Solution {
public int rob(int[] nums) {
int len = nums.length;
if (len == 0) return 0;
else if (len == 1) return nums[0];
else if (len == 2) return Math.max(nums[0],nums[1]);
int[] result = new int[3]; //存放选择的结果
result[0] = nums[0];
result[1] = Math.max(nums[0],nums[1]);
for(int i=2;i<len;i++){
result[2] = Math.max(result[0]+nums[i],result[1]);
result[0] = result[1];
result[1] = result[2];
}
return result[2];
}
}
// 进一步对滚动数组的空间优化 dp数组只存与计算相关的两次数据
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
// 初始化dp数组
// 优化空间 dp数组只用2格空间 只记录与当前计算相关的前两个结果
int[] dp = new int[2];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
int res = 0;
// 遍历
for (int i = 2; i < nums.length; i++) {
res = Math.max((dp[0] + nums[i]) , dp[1] );
dp[0] = dp[1];
dp[1] = res;
}
// 输出结果
return dp[1];
}
}
213. House Robber II
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night.
Given an integer array nums
representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Example 1:
Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.
Example 2:
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.
Example 3:
Input: nums = [1,2,3]
Output: 3
Constraints:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
思路
这道题目和198.打家劫舍是差不多的,唯一区别就是成环了。
对于一个数组,成环的话主要有如下三种情况:
- 情况一:考虑不包含首尾元素
-
情况二:考虑包含首元素,不包含尾元素
-
情况三:考虑包含尾元素,不包含首元素
注意我这里用的是"考虑",例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。
而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。
分析到这里,本题其实比较简单了。 剩下的和198.打家劫舍就是一样的了。
C++解法
注意注释中的情况二情况三,以及把198.打家劫舍的代码抽离出来了。完整代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
int result1 = robRange(nums, 0, nums.size() - 2); // 情况二
int result2 = robRange(nums, 1, nums.size() - 1); // 情况三
return max(result1, result2);
}
// 198.打家劫舍的逻辑
int robRange(vector<int>& nums, int start, int end) {
if (end == start) return nums[start];
vector<int> dp(nums.size());
dp[start] = nums[start];
dp[start + 1] = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[end];
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
Java解法
class Solution {
public int robLinearArray(int[] nums) {
int[] dp = new int[nums.length + 1];
dp[0] = nums[0];
if(nums.length == 1){
return dp[0];
}
if(nums.length > 1){
dp[1] = Math.max(nums[0], nums[1]);
for(int j = 2; j < nums.length; j++){
dp[j] = Math.max(dp[j - 1], dp[j - 2] + nums[j]);
}
}
return dp[nums.length - 1];
}
public int rob(int[] nums) {
if(nums.length == 1){
return nums[0];
}
int[] resultWithoutFirst = Arrays.copyOfRange(nums, 1, nums.length);
int[] resultWithoutLast = Arrays.copyOfRange(nums, 0, nums.length - 1);
int maxLeft = robLinearArray(resultWithoutFirst);
int maxRight = robLinearArray(resultWithoutLast);
return Math.max(maxLeft, maxRight);
}
}
337. House Robber III
The thief has found himself a new place for his thievery again. There is only one entrance to this area, called root
.
Besides the root
, each house has one and only one parent house. After a tour, the smart thief realized that all houses in this place form a binary tree. It will automatically contact the police if two directly-linked houses were broken into on the same night.
Given the root
of the binary tree, return the maximum amount of money the thief can rob without alerting the police.
Example 1:
Input: root = [3,2,3,null,3,null,1]
Output: 7
Explanation: Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
Example 2:
Input: root = [3,4,5,1,3,null,1]
Output: 9
Explanation: Maximum amount of money the thief can rob = 4 + 5 = 9.
Constraints:
- The number of nodes in the tree is in the range
[1, 10^4]
. 0 <= Node.val <= 10^4
思路
对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。
本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算。
与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。
如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(注意这里说的是“考虑”)
动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。
这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
- 确定递归函数的参数和返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。
参数为当前节点,代码如下:
vector<int> robTree(TreeNode* cur) {
其实这里的返回数组就是dp数组。
所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
所以本题dp数组就是一个长度为2的数组!
那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢?
别忘了在递归的过程中,系统栈会保存每一层递归的参数。
如果还不理解的话,就接着往下看,看到代码就理解了哈。
- 确定终止条件
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
if (cur == NULL) return vector<int>{0, 0};
这也相当于dp数组的初始化
- 确定遍历顺序
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
代码如下:
// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
4. 确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];
(如果对下标含义不理解就再回顾一下dp数组的含义)
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
代码如下:
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
- 举例推导dp数组
以示例1为例,dp数组状态如下:(注意用后序遍历的方式推导)
最后头结点就是取下标0 和下标1的最大值就是偷得的最大金钱。
C++解法
暴力递归代码如下:
class Solution {
public:
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
return max(val1, val2);
}
};
- 时间复杂度:O(n^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多
- 空间复杂度:O(log n),算上递推系统栈的空间
当然以上代码超时了,这个递归的过程中其实是有重复计算了。
我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。
记忆化递推
所以可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。
代码如下:
class Solution {
public:
unordered_map<TreeNode* , int> umap; // 记录计算过的结果
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
umap[root] = max(val1, val2); // umap记录一下结果
return max(val1, val2);
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(log n),算上递推系统栈的空间
动态规划
递归三部曲与动规五部曲分析完毕,C++代码如下:
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = robTree(root);
return max(result[0], result[1]);
}
// 长度为2的数组,0:不偷,1:偷
vector<int> robTree(TreeNode* cur) {
if (cur == NULL) return vector<int>{0, 0};
vector<int> left = robTree(cur->left);
vector<int> right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
- 时间复杂度:O(n),每个节点只遍历了一次
- 空间复杂度:O(log n),算上递推系统栈的空间
Java解法
class Solution {
// 1.递归去偷,超时
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);
}
return Math.max(money, rob(root.left) + rob(root.right));
}
// 2.递归去偷,记录状态
// 执行用时:3 ms , 在所有 Java 提交中击败了 56.24% 的用户
public int rob1(TreeNode root) {
Map<TreeNode, Integer> memo = new HashMap<>();
return robAction(root, memo);
}
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;
}
// 3.状态标记递归
// 执行用时:0 ms , 在所有 Java 提交中击败了 100% 的用户
// 不偷:Max(左孩子不偷,左孩子偷) + Max(右孩子不偷,右孩子偷)
// root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) +
// Math.max(rob(root.right)[0], rob(root.right)[1])
// 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
// root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
public int rob3(TreeNode root) {
int[] res = robAction1(root);
return Math.max(res[0], res[1]);
}
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;
}
}
2560. House Robber IV
There are several consecutive houses along a street, each of which has some money inside. There is also a robber, who wants to steal money from the homes, but he refuses to steal from adjacent homes.
The capability of the robber is the maximum amount of money he steals from one house of all the houses he robbed.
You are given an integer array nums
representing how much money is stashed in each house. More formally, the ith
house from the left has nums[i]
dollars.
You are also given an integer k
, representing the minimum number of houses the robber will steal from. It is always possible to steal at least k
houses.
Return the minimum capability of the robber out of all the possible ways to steal at least k
houses.
Example 1:
Input: nums = [2,3,5,9], k = 2
Output: 5
Explanation:
There are three ways to rob at least 2 houses:
- Rob the houses at indices 0 and 2. Capability is max(nums[0], nums[2]) = 5.
- Rob the houses at indices 0 and 3. Capability is max(nums[0], nums[3]) = 9.
- Rob the houses at indices 1 and 3. Capability is max(nums[1], nums[3]) = 9. Therefore, we return min(5, 9, 9) = 5.
Example 2:
Input: nums = [2,7,9,3,1], k = 2
Output: 2
Explanation: There are 7 ways to rob the houses. The way which leads to minimum capability is to rob the house at index 0 and 4. Return max(nums[0], nums[4]) = 2.
Constraints:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^9
1 <= k <= (nums.length + 1)/2
思路
Can we use binary search to find the minimum value of a non-contiguous subsequence of a given size k?
Initialize the search range with the minimum and maximum elements of the input array.
Use a check function to determine if it is possible to select k non-consecutive elements that are less than or equal to the current "guess" value.
Adjust the search range based on the outcome of the check function, until the range converges and the minimum value is found.
方法一:二分查找
二分查找答案:找到最小的 x ,使得数组 nums 存在至少 k 个不超过 x 的数。
题目需要获取窃取至少 k 间房屋时小偷的最小窃取能力,属于常见的最小化最大值问题。
记当前偷取的房屋数目为 count,遍历数组 nums,假设当前遍历的房屋的金额为 x,如果 x≤y 成立,且上一遍历的房屋没有被偷取,那么令偷取的房屋数目 count 加 1,表示该房屋被偷取。 遍历结束后 f(y)=count,显然 f(y) 是非递减函数。
那么我们可以使用二分查找的方法,找到满足 f(y)≥k 的最小 y,即题目所求的小偷最小窃取能力。
C++解法
具体二分查找算法如下:
初始时 lower=numsmin,upper=numsmax。
令 middle=⌊(lower+upper)/2⌋,如果 f(middle)≥k,那么 upper=middle−1;否则 lower=middle+1。
当 lower≤upper 时,继续执行步骤 2;否则返回 lower 为结果。
class Solution {
public:
int minCapability(vector<int>& nums, int k) {
int lower = *min_element(nums.begin(), nums.end());
int upper = *max_element(nums.begin(), nums.end());
while (lower <= upper) {
int middle = (lower + upper) / 2;
int count = 0;
bool visited = false;
for (int x : nums) {
if (x <= middle && !visited) {
count++;
visited = true;
} else {
visited = false;
}
}
if (count >= k) {
upper = middle - 1;
} else {
lower = middle + 1;
}
}
return lower;
}
};
复杂度分析
时间复杂度:O(nlogT),其中 n 是数组 nums 的长度,T 是数组最大值与最小值之差。二分查找的次数为 O(logT),每次查找需要 O(n)。
空间复杂度:O(1)。
另一种解法如下所示:
class Solution {
public:
bool canRob(vector<int>& nums, int mid, int k) {
int count = 0, n = nums.size();
for (int i = 0; i < n; i++) {
if (nums[i] <= mid) {
count++;
i++;
}
}
return count >= k;
}
int minCapability(vector<int>& nums, int k) {
int left = 1, right = *max_element(nums.begin(), nums.end()), ans = right;
while (left <= right) {
int mid = (left + right) / 2;
if (canRob(nums, mid, k)) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
};
Java解法
class Solution {
public boolean canRob(int[] nums, int mid, int k) {
int count = 0, n = nums.length;
for (int i = 0; i < n; i++) {
if (nums[i] <= mid) {
count++;
i++;
}
}
return count >= k;
}
public int minCapability(int[] nums, int k) {
int left = 1, right = Arrays.stream(nums).max().getAsInt(), ans = right;
while (left <= right) {
int mid = (left + right) / 2;
if (canRob(nums, mid, k)) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
}