每日一题——打家劫舍

打家劫舍(一)与打家劫舍(二)动态规划解法详解

打家劫舍(一)

问题描述

你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金。不能偷相邻的两家 。给定一个整数数组 nums,数组中的元素表示每个房间存有的现金数额,计算在不被发现的前提下最多的偷窃金额。

数据范围

数组长度满足 (1 \leq n \leq 2 \times 10^5),数组中每个值满足 (1 \leq \text{nums}[i] \leq 5000)。


示例

输入 输出 说明
[1, 2, 3, 4] 6 偷第 2、4 个房间
[1, 3, 6] 7 偷第 1、3 个房间
[2, 10, 5] 10 偷第 2 个房间

解题思路

动态规划
  1. 状态定义

    • dp[i] 表示考虑前 i 个房间时的最大偷窃金额。
  2. 状态转移方程

    • 当前房间有两种选择:
      • 不偷 :继承前一个房间的最大金额(dp[i-1])。
      • :当前房间金额 + 前前一个房间的最大金额(dp[i-2] + nums[i])。
    • 因此:
      [
      dp[i] = \max(dp[i-1], \ dp[i-2] + \text{nums}[i])
      ]
  3. 边界条件

    • dp[0] = nums[0]:只有一个房间时只能偷它。
    • dp[1] = \max(nums[0], nums[1]):两个房间时偷金额较大的。

代码实现

c 复制代码
int rob(int* nums, int numsLen) {
    // 如果数组长度小于等于0,说明没有房子可以偷,直接返回0
    if (numsLen <= 0) return 0;

    // 如果数组长度为1,说明只有一间房子,直接返回这间房子的金额
    if (numsLen == 1) return nums[0];

    // 如果数组长度为2,说明有两间房子,选择金额较大的那间房子
    if (numsLen == 2) return (nums[0] > nums[1]) ? nums[0] : nums[1];

    // 定义一个动态规划数组dp,dp[i]表示到第i间房子为止,小偷能偷到的最大金额
    int dp[numsLen];

    // 初始化dp数组的前两个值
    // 如果只有一间房子,最大金额就是这间房子的金额
    dp[0] = nums[0];
    // 如果有两间房子,最大金额是这两间房子中金额较大的那个
    dp[1] = (nums[0] > nums[1]) ? nums[0] : nums[1];

    // 遍历数组,从第三间房子开始计算
    for (int i = 2; i < numsLen; i++) {
        // 对于第i间房子,有两种选择:
        // 1. 不偷第i间房子,最大金额就是dp[i-1](即前一间房子的最大金额)
        // 2. 偷第i间房子,最大金额是dp[i-2] + nums[i](即前两间房子的最大金额加上当前房子的金额)
        // dp[i]的值是这两种选择中的较大值
        dp[i] = (dp[i - 1] > (dp[i - 2] + nums[i])) ? dp[i - 1] : (dp[i - 2] + nums[i]);
    }

    // 返回dp数组的最后一个值,即所有房子的最大金额
    return dp[numsLen - 1];
}

复杂度分析

  • 时间复杂度:(O(n)),遍历一次数组。
  • 空间复杂度:(O(n)),存储动态规划数组。

打家劫舍(二)

问题描述

沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻 。给定一个长度为 n 的整数数组 nums,计算在不被发现的前提下最多的偷窃金额。

数据范围:与打家劫舍(一)相同。


示例

输入 输出 说明
[1, 2, 3, 4] 6 偷第 2、4 个房间
[1, 3, 6] 6 偷第 3 个房间

解题思路

环形问题的拆分

由于首尾房间相邻,问题可拆分为两种情况:

  1. 不偷第一个房间 :计算区间 [1, n-1] 的最大偷窃金额。
  2. 不偷最后一个房间 :计算区间 [0, n-2] 的最大偷窃金额。

最终结果为两种情况的最大值。


代码实现

c 复制代码
// 辅助函数:计算从 start 到 end 的最大偷窃金额
int robRange(int* nums, int start, int end) {
    int n = end - start + 1; // 计算从 start 到 end 的区间长度
    if (n == 1) return nums[start]; // 如果区间内只有一间房子,直接返回这间房子的金额

    // 定义动态规划数组 dp,dp[i] 表示从 start 开始到第 i 间房子的最大偷窃金额
    int dp[n];
    dp[0] = nums[start]; // 初始化第一间房子的最大金额
    dp[1] = (nums[start] > nums[start + 1]) ? nums[start] : nums[start + 1]; // 初始化前两间房子的最大金额

    // 遍历从第 3 间房子到最后一间房子
    for (int i = 2; i < n; i++) {
        int current = start + i; // 当前房子在原数组中的索引
        // 状态转移方程:选择当前房子或不选择当前房子的最大金额
        dp[i] = (dp[i - 1] > (dp[i - 2] + nums[current])) ? dp[i - 1] : (dp[i - 2] + nums[current]);
    }

    // 返回从 start 到 end 的最大偷窃金额
    return dp[n - 1];
}
int rob(int* nums, int numsLen) {
    // 如果只有一间房子,直接返回这间房子的金额
    if (numsLen == 1) return nums[0];
    // 如果有两间房子,返回两间房子中金额较大的那个
    if (numsLen == 2) return (nums[0] > nums[1]) ? nums[0] : nums[1];

    // 分两种情况讨论:
    // 1. 不偷最后一间房子,计算从第 0 间到第 (numsLen - 2) 间房子的最大金额
    int case1 = robRange(nums, 0, numsLen - 2);
    // 2. 不偷第一间房子,计算从第 1 间到第 (numsLen - 1) 间房子的最大金额
    int case2 = robRange(nums, 1, numsLen - 1);

    // 返回两种情况中的较大值
    return (case1 > case2) ? case1 : case2;
}

复杂度分析

  • 时间复杂度:(O(n)),两次遍历数组。
  • 空间复杂度:(O(n)),存储动态规划数组。

总结

问题 动态规划状态转移方程 关键点
打家劫舍(一) dp[i] = max(dp[i-1], dp[i-2] + nums[i]) 线性数组,直接遍历
打家劫舍(二) 拆分两种情况求最大值 环形数组,避免首尾同时偷窃
相关推荐
byte轻骑兵17 分钟前
【嵌入式Linux应用开发基础】进程间通信(2):消息队列
linux·c语言·嵌入式软件
Ypuyu2 小时前
[H数据结构] lc1206. 设计跳表(模拟+数据结构+跳表实现+优秀博文)
数据结构
WIFI_BT_DEV2 小时前
Linux设备驱动开发-中断
linux·c语言·arm开发·驱动开发·嵌入式硬件·硬件架构·gnu
姚先生973 小时前
LeetCode 贪心算法经典题目 (C++实现)
c++·leetcode·贪心算法
CodeJourney.3 小时前
DeepSeek在MATLAB上的部署与应用
数据库·人工智能·算法·架构
苦学编程的谢3 小时前
链表(LinkedList)面试题
数据结构·链表
烟雨迷3 小时前
八大排序算法(C语言实现)
c语言·数据结构·学习·算法·排序算法
emmmmXxxy3 小时前
leetcode刷题-动态规划08
算法·leetcode·动态规划
共享家95274 小时前
深入理解C语言中的位段
c语言