前言
读完这篇文章你就可以顺手解决以下题目:
198. 打家劫舍 | 213. 打家劫舍II |
---|---|
337. 打家劫舍III | 740. 删除并获得点数 |
默认有动态规划基础的同学,不适合新手小白。
打家劫舍I
力扣链接:https://leetcode.cn/problems/house-robber/description/
Java
java
class Solution {
public int rob(int[] nums) {
// 边界条件:若房子数组为空或无房子可偷,直接返回 0
if(nums == null || nums.length == 0) return 0;
// 若只有一间房子,则直接返回该房子的金额
if(nums.length == 1) return nums[0];
int n = nums.length;
// 定义 dp 数组,dp[i] 表示偷前 i 间房子能获得的最高金额
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
// 状态转移:对于每间房子,可以选择偷或不偷,递推 dp[i] 的最大值
for(int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
}
代码说明
- 初始化 :
dp[0]
表示偷第一间房子的金额,dp[1]
表示偷前两间房子的最大金额。 - 状态转移 :对于每一间房子,可以选择:
- 不偷当前房子 :延续前一间房子的最大金额
dp[i-1]
。 - 偷当前房子 :加上隔一间房子的金额
dp[i-2] + nums[i]
。
- 不偷当前房子 :延续前一间房子的最大金额
- 返回结果 :
dp[n-1]
表示偷完所有房子后的最高金额。
C++
cpp
class Solution {
public:
int rob(vector<int>& nums) {
// 边界条件:若房子数组为空或无房子可偷,直接返回 0
if(nums.empty()) return 0;
// 若只有一间房子,则直接返回该房子的金额
if(nums.size() == 1) return nums[0];
int n = nums.size();
// 定义 dp 数组,dp[i] 表示偷前 i 间房子能获得的最高金额
vector<int> dp(n);
// 初始化:偷第一间房子能获得的金额就是 nums[0]
dp[0] = nums[0];
// 偷前两间房子能获得的最高金额为两者中的较大值
dp[1] = max(nums[0], nums[1]);
// 状态转移:对于每间房子,可以选择偷或不偷,递推 dp[i] 的最大值
for(int i = 2; i < n; i++) {
// 偷第 i 间房子:dp[i-2] + nums[i](即不偷前一间房子,偷当前房子)
// 不偷第 i 间房子:dp[i-1](即沿用前一间房子的最大金额)
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
// 返回能偷到的最高金额,即偷完所有房子后的最大金额
return dp[n - 1];
}
};
打家劫舍II
力扣链接:https://leetcode.cn/problems/house-robber-ii/description/
这道题与 打家劫舍I 不同之处是,这道题中的房屋是首尾相连的,第一间房屋和最后一间房屋相邻,因此第一间房屋和最后一间房屋不能在同一晚上偷窃。相当于是一种环形结构。
其实也很简单。把 打家劫舍II
分成两个 打家劫舍I
去做:
有 n 个房间,编号为 0...n - 1
,分成两种情况:
- 偷编号为
0...n - 2
的 n - 1 个房间; - 偷编号为
1...n - 1
的 n - 1 个房间;
最后的结果是二者的最大值~
Java
java
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
return Math.max(robAction(Arrays.copyOfRange(nums,0,nums.length-1)),
robAction(Arrays.copyOfRange(nums,1,nums.length)));
}
// 打家劫舍I 的代码片段,直接粘过来
public int robAction(int[] nums) {
if(nums == null || nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i<n;i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
}
代码说明
这里需要注意的就是这个Arrays.copyOfRange
方法了:
Arrays.copyOfRange
是 Java 中的一个实用方法,用于从一个数组中复制一个范围的元素到一个新的数组中。这个方法常用于处理数组的子集,尤其是当你需要操作数组的一部分而不是整个数组时。
Arrays.copyOfRange
的定义如下:
java
public static <T> T[] copyOfRange(T[] original, int from, int to)
这里的 from
参数指定了要复制的第一个元素的索引(包含),to
参数指定了最后一个元素之后的位置(不包含)。也就是说,to
参数并不是实际要复制的最后一个元素的索引,而是该索引之后的位置。
比如:
java
Arrays.copyOfRange(nums, 0, nums.length - 1);
这行代码创建了一个不包含最后一个元素的新数组。例如,如果 nums
是 [1, 2, 3, 4]
,那么结果将是 [1, 2, 3]
。
C++
cpp
class Solution {
public:
int rob(vector<int>& nums) {
// 边界条件:如果数组为空或只有一间房子
if(nums.empty()) return 0;
if(nums.size() == 1) return nums[0];
// 比较两种情况:不偷最后一间房子,或不偷第一间房子
return std::max(robAction(std::vector<int>(nums.begin(), nums.end() - 1)),
robAction(std::vector<int>(nums.begin() + 1, nums.end())));
}
private:
// 辅助函数 robAction,相当于单排房屋的打家劫舍问题
int robAction(const std::vector<int>& nums) {
// 边界条件处理
if(nums.empty()) return 0;
if(nums.size() == 1) return nums[0];
int n = nums.size();
std::vector<int> dp(n);
// 初始化前两间房子的最高收益
dp[0] = nums[0];
dp[1] = std::max(nums[0], nums[1]);
// 状态转移
for(int i = 2; i < n; i++) {
dp[i] = std::max(dp[i - 1], dp[i - 2] + nums[i]);
}
// 返回能偷到的最高金额
return dp[n - 1];
}
};
打家劫舍III
力扣链接:https://leetcode.cn/problems/house-robber-iii/description/
这个题就有些意思了,不同于前两道题的数组DP ,这是一道树形DP的题。话不多说,直接看代码:
Java
java
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] res = dfs(root);
return Math.max(res[0],res[1]); // 根节点选或不选的最大值
}
public int[] dfs(TreeNode root){
if(root == null) return new int[]{0, 0}; // 没有节点,怎么选都是0
int[] left = dfs(root.left); // 递归左子树
int[] right = dfs(root.right); // 递归右子树
int rob = left[1] + right[1] + root.val; // 选根节点,不选左右节点
int notRob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); // 不选根节点
return new int[]{rob, notRob};
}
}
代码说明
-
rob(TreeNode root)
:- 调用
dfs(root)
,返回一个数组res
,其中res[0]
表示不偷根节点的最大金额,res[1]
表示偷根节点的最大金额。 - 最后返回
Math.max(res[0], res[1])
,即偷或不偷根节点的最大收益。
- 调用
-
dfs(TreeNode root)
:- 若当前节点为空,返回
{0, 0}
。 - 递归计算左右子树的最大收益,结果保存在
left
和right
中。 - 偷当前节点 :最大收益为
root.val + left[0] + right[0]
(子节点不偷)。 - 不偷当前节点 :最大收益为
Math.max(left[0], left[1]) + Math.max(right[0], right[1])
(子节点可偷或不偷)。 - 返回
{rob, notRob}
表示偷与不偷当前节点的最大收益。
- 若当前节点为空,返回
C++
cpp
class Solution {
public:
int rob(TreeNode* root) {
auto res = dfs(root);
return std::max(res[0], res[1]); // 根节点选或不选的最大值
}
private:
std::vector<int> dfs(TreeNode* root) {
if (!root) return {0, 0}; // 没有节点,怎么选都是0
auto left = dfs(root->left); // 递归左子树
auto right = dfs(root->right); // 递归右子树
int rob = left[1] + right[1] + root->val; // 选根节点,不选左右子节点
int notRob = std::max(left[0], left[1]) + std::max(right[0], right[1]); // 不选根节点
return {rob, notRob};
}
};
删除并获得点数
力扣链接:https://leetcode.cn/problems/delete-and-earn/description/
基本等同于小偷打家劫舍 ,可以取参考上面题解。但很多同学被nums[i] + 1
和nums[i] -1
唬住了,思考的更复杂了。这题类似打家劫舍的思路------不能选相邻的元素来最大化收益。
我们可以用一个数组 sum 记录数组 nums 中所有相同元素之和。索引为数组中的元素值,sum[i] 表示选择值 i 时能获得的总点数。通过遍历 nums 数组,依次将 nums[i] 的值累加到 sum[nums[i]] 中。这样便将问题转化为一个"打家劫舍"问题,即选择非连续索引的最大和。
Java
java
class Solution {
public int deleteAndEarn(int[] nums) {
int max = 0;
// 找出数组中最大值
for(int i : nums){
max = Math.max(max, i);
}
// 构建 sum 数组,sum[i] 表示选择值 i 时获得的总点数
int[] sum = new int[max + 1];
for(int val : nums){
sum[val] += val;
}
// 使用打家劫舍的动态规划方法计算最大点数
return rob(sum);
}
public int rob(int[] nums) {
// 边界条件:数组为空或只有一个元素
if(nums == null || nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
int n = nums.length;
// dp 数组:dp[i] 表示偷取前 i 个元素获得的最大点数
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
// 动态规划计算最大点数
for(int i = 2; i < n; i++){
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
// 返回最大点数
return dp[n - 1];
}
}
代码说明
-
构建
sum
数组 :找到nums
中的最大值max
,创建长度为max + 1
的数组sum
,将nums
中每个数的总和累加到对应的索引中。sum[i]
表示选择i
这一值的总收益。 -
调用
rob
计算最大收益 :rob(sum)
使用"打家劫舍"动态规划思想,计算不相邻元素的最大和。 -
rob
方法(动态规划) :设置dp
数组,dp[i]
表示选择到第i
个元素时的最大收益。使用dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
更新最大值,最终返回dp[n - 1]
作为最大收益。
C++
cpp
class Solution {
public:
int deleteAndEarn(std::vector<int>& nums) {
int maxVal = 0;
// 找出数组中最大值
for(int num : nums) {
maxVal = std::max(maxVal, num);
}
// 构建 sum 数组,sum[i] 表示选择值 i 时获得的总点数
std::vector<int> sum(maxVal + 1, 0);
for(int val : nums) {
sum[val] += val;
}
// 使用打家劫舍的动态规划方法计算最大点数
return rob(sum);
}
private:
int rob(const std::vector<int>& nums) {
// 边界条件:数组为空或只有一个元素
if(nums.empty()) return 0;
if(nums.size() == 1) return nums[0];
int n = nums.size();
std::vector<int> dp(n, 0);
dp[0] = nums[0];
dp[1] = std::max(nums[0], nums[1]);
// 动态规划计算最大点数
for(int i = 2; i < n; i++) {
dp[i] = std::max(dp[i - 1], dp[i - 2] + nums[i]);
}
// 返回最大点数
return dp[n - 1];
}
};
结语
更多经典面试算法题可见:👉点击这里