LeetCode动态规划篇:经典打家劫舍问题

前言

读完这篇文章你就可以顺手解决以下题目:

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];
    }
}

代码说明

  1. 初始化dp[0] 表示偷第一间房子的金额,dp[1] 表示偷前两间房子的最大金额。
  2. 状态转移 :对于每一间房子,可以选择:
    • 不偷当前房子 :延续前一间房子的最大金额 dp[i-1]
    • 偷当前房子 :加上隔一间房子的金额 dp[i-2] + nums[i]
  3. 返回结果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};
    }
}

代码说明

  1. rob(TreeNode root)

    • 调用 dfs(root),返回一个数组 res,其中 res[0] 表示不偷根节点的最大金额,res[1] 表示偷根节点的最大金额。
    • 最后返回 Math.max(res[0], res[1]),即偷或不偷根节点的最大收益。
  2. dfs(TreeNode root)

    • 若当前节点为空,返回 {0, 0}
    • 递归计算左右子树的最大收益,结果保存在 leftright 中。
    • 偷当前节点 :最大收益为 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] + 1nums[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];
    }
}

代码说明

  1. 构建 sum 数组 :找到 nums 中的最大值 max,创建长度为 max + 1 的数组 sum,将 nums 中每个数的总和累加到对应的索引中。sum[i] 表示选择 i 这一值的总收益。

  2. 调用 rob 计算最大收益rob(sum) 使用"打家劫舍"动态规划思想,计算不相邻元素的最大和。

  3. 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];
    }
};

结语

更多经典面试算法题可见:👉点击这里

相关推荐
云卓科技3 分钟前
无人机之任务分配算法篇
科技·算法·机器人·无人机·交互·制造
DaLi Yao5 分钟前
【笔记】复数基础&&复数相乘的物理意义:旋转+缩放
学习·算法·ai·矩阵
云卓科技6 分钟前
无人机之目标检测算法篇
人工智能·科技·算法·目标检测·计算机视觉·机器人·无人机
lzmlzm891 小时前
太阳能板表面缺陷裂缝等识别系统:精准目标定位
算法
__AtYou__1 小时前
Golang | Leetcode Golang题解之第507题完美数
leetcode·golang·题解
陈序缘2 小时前
Rust 力扣 - 238. 除自身以外数组的乘积
开发语言·后端·算法·leetcode·rust
AlexMercer10122 小时前
[C++ 核心编程]笔记 4.2.6 初始化列表
开发语言·数据结构·c++·笔记·算法
zzzhpzhpzzz2 小时前
设计模式——享元模式
算法·设计模式·享元模式
夜雨翦春韭3 小时前
【代码随想录Day54】图论Part06
java·开发语言·数据结构·算法·leetcode·图论
玉树临风ives3 小时前
2024 CSP-J 题解
c++·算法·深度优先·动态规划