前言
读完这篇文章你就可以顺手解决以下题目:
| 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];
    }
};
        结语
更多经典面试算法题可见:👉点击这里