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

结语

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

相关推荐
猿究院--王升1 小时前
jvm三色标记
java·jvm·算法
一车小面包2 小时前
逻辑回归 从0到1
算法·机器学习·逻辑回归
tt5555555555553 小时前
字符串与算法题详解:最长回文子串、IP 地址转换、字符串排序、蛇形矩阵与字符串加密
c++·算法·矩阵
元亓亓亓4 小时前
LeetCode热题100--101. 对称二叉树--简单
算法·leetcode·职场和发展
不会学习?4 小时前
算法03 归并分治
算法
NuyoahC5 小时前
笔试——Day43
c++·算法·笔试
2301_821919925 小时前
决策树8.19
算法·决策树·机器学习
秋难降5 小时前
别再用暴力排序了!大小顶堆让「取极值」效率飙升至 O (log n)
python·算法·排序算法
学行库小秘6 小时前
基于门控循环单元的数据回归预测 GRU
人工智能·深度学习·神经网络·算法·回归·gru
_meow_6 小时前
数学建模 15 逻辑回归与随机森林
算法·数学建模·逻辑回归