由于之前写过打家劫舍系列,这里直接弄个合集,后面应该还有个iv。
目录
198. 打家劫舍
213. 打家劫舍 II
337. 打家劫舍 III
2560. 打家劫舍 IV
198. 打家劫舍
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
实现代码与解析:
方法一:
class Solution {
public:
int rob(vector<int>& nums) {
vector<vector<int>> f(nums.size(), vector<int>(2, 0));
// f[i][0] 第i个房间不偷的最大金额,f[i][1] 第i个房间偷的最大金额
f[0][0] = 0;
f[0][1] = nums[0];
for (int i = 1; i < nums.size(); i++)
{
f[i][0] = max (f[i - 1][0], f[i - 1][1]);
f[i][1] = f[i - 1][0] + nums[i];
}
return max(f[nums.size()-1][0], f[nums.size()-1][1]);
}
};
方法二:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 1) {
return nums[0];
}
vector<int> dp = vector<int>(nums.size(), 0);
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];
}
};
原理思路:
两种方法,其实就是dp数组定义的含义不同,都是可以解决此题的。
方法一的dp含义:
f[i][0]
表示第i个房间不偷的最大金额,最大金额等于前一个房间不偷的最大金额(f[i-1][0]
)或者前一个房间偷的最大金额(f[i-1][1]
)中的较大值。f[i][1]
表示第i个房间偷的最大金额,只能选择偷第i个房间,那么最大金额等于前一个房间不偷的最大金额(f[i-1][0]
)加上第i个房间的财物数量(nums[i]
)。
方法二的dp含义:
dp[i]
表示抢劫前i个房屋所能获得的最大金额。可以选择抢劫第i个房屋,那么最大金额等于前两个房屋的最大金额(dp[i-2]
)加上第i个房屋的财物数量(nums[i]
)与不抢劫第i个房屋的最大金额(dp[i-1]
)中的较大值。
213. 打家劫舍 II
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3] 输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
实现代码与解析:
dp
class Solution {
public:
int robRange(vector<int>& nums,int start,int end)
{
if(start == end) 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 - 1], dp[i - 2] + nums[i]);
}
return dp[end];
}
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); // 不选取第一个房间的最大价值
int result = max(result1, result2);
return result;
}
};
原理思路:
和上一题完全相似,这里我们不考虑第一个房间或不考虑最后一个房间分别dp一下,取一个max即可。
337. 打家劫舍 III
题目描述:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1] 输出: 7 解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1] 输出: 9 解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
- 树的节点数在
[1, 104]
范围内 0 <= Node.val <= 104
实现代码与解析:
树形dp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> dp(TreeNode* cur)
{
if (cur == NULL) return{0, 0};
// 0 不偷,1偷
vector<int> left = dp(cur->left);
vector<int> right = dp(cur->right);
vector<int> res(2, 0);
res[0] = max({left[1] + right[1], left[0] + right[0], left[0] + right[1], left[1] + right[0]}); // 不偷此房间的最大金额,注意误区,子树房间不一定偷了才是最大。
res[1] = left[0] + right[0] + cur->val;
return res;
}
int rob(TreeNode* root) {
vector<int> res = dp(root);
return max(res[0], res[1]);
}
};
原理思路:
-
在
dp
函数中,首先检查当前节点是否为NULL
,如果是,则返回一个包含两个零的向量{0, 0}
,表示没有金额可偷。 -
接下来,递归调用
dp
函数计算左子树和右子树的最大金额,分别存储在left
和right
向量中。 -
然后,定义一个名为
res
的向量,用于存储当前节点可以偷取的最大金额,其中res[0]
表示不偷当前节点,res[1]
表示偷当前节点。 -
计算
res[0]
的值,考虑了四种情况:- 不偷当前节点,同时也不偷左子树和右子树:
left[0] + right[0]
- 不偷当前节点,但偷左子树:
left[1] + right[0]
- 不偷当前节点,但偷右子树:
left[0] + right[1]
- 偷当前节点,同时不偷左子树和右子树:
left[0] + right[0] + cur->val
取这四种情况中的最大值作为
res[0]
的值,表示不偷当前节点的最大金额。 - 不偷当前节点,同时也不偷左子树和右子树:
-
计算
res[1]
的值,即偷当前节点,同时不偷左子树和右子树,即left[0] + right[0] + cur->val
。 -
最后,函数返回
res
向量,其中res[0]
和res[1]
分别表示不偷和偷当前节点的最大金额。 -
在
rob
函数中,调用dp
函数来计算根节点root
的最大金额,并返回其中的较大值,即max(res[0], res[1])
,这是整个问题的解。
使用动态规划的思想,通过递归遍历二叉树的节点来计算最大金额。它在每个节点上都考虑了两种情况:偷取当前节点和不偷取当前节点,然后根据这两种情况的结果计算最大金额。最后,返回根节点的最大金额,即问题的解。
2560. 打家劫舍 IV
题目描述:
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums
表示每间房屋存放的现金金额。形式上,从左起第 i
间房屋中放有 nums[i]
美元。
另给你一个整数 k
,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k
间房屋。
返回小偷的 最小 窃取能力。
示例 1:
输入:nums = [2,3,5,9], k = 2 输出:5 解释: 小偷窃取至少 2 间房屋,共有 3 种方式: - 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。 - 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。 - 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。 因此,返回 min(5, 9, 9) = 5 。
示例 2:
输入:nums = [2,7,9,3,1], k = 2 输出:2 解释:共有 7 种窃取方式。窃取能力最小的情况所对应的方式是窃取下标 0 和 4 处的房屋。返回 max(nums[0], nums[4]) = 2 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= k <= (nums.length + 1)/2
实现代码与解析:
二分
class Solution {
public:
bool check(vector<int>& nums, int mid, int k) {
int flag = 1; // 判断能否选取
int count = 0; // 当前选取个数
for (auto t: nums)
{
if (t <= mid && flag)
{
count++;
flag = 0;
}
else
{
flag = 1;
}
}
if (count < k) return false;
else return true;
}
int minCapability(vector<int>& nums, int k) {
int l = *min_element(nums.begin(), nums.end()); // 最小值
int r = *max_element(nums.begin(), nums.end()); // 最大值
while(l < r)
{
int mid = (l + r) >> 1;
if (check(nums, mid, k)) r = mid;
else l = mid + 1;
}
return l;
}
}
原理思路:
解析题目,其实是求在选取大于k个不相邻房间的情况中,找出每种选法的最大值中的最小值。
check中,小于等于mid且此位置可以选取才能偷,此种选取最后个数大于等于k返回true,反之返回false。