198-动态规划-打家劫舍

数组 | 动态规划

入门的解法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

 var rob = function (nums) {
  n = nums.length;
  function dfs(i) {
    if (i < 0) {
      return 0;
    }
    let res = Math.max(dfs(i - 1), dfs(i - 2) + nums[i]);
    return res;
  }
  return dfs(n - 1);
};

我们来详细解析这个“打家劫舍”(House Robber)问题的解决方案。这个问题是动态规划中一个经典的例子,涉及到在一维数组中选择不相邻的元素以获得最大和。我们将逐步分析代码,理解其中的逻辑和动态规划的思想。

问题描述

题目:给定一个非负整数数组 nums,代表每间房屋内存放的现金。小偷不能同时偷窃相邻的房屋。你的目标是计算小偷在不触发报警的情况下,能够偷窃到的最高金额。

示例

  • 输入:nums = [2, 7, 9, 3, 1]
  • 输出:12(偷窃房屋 1 和 3 的现金,将得到 2 + 9 + 1 = 12)

动态规划思路

在解决这个问题时,我们可以使用动态规划的方式来优化我们的选择。基本思路如下:

  1. 状态定义:假设 dp[i] 表示偷窃到第 i 间房屋时能够获得的最大金额。

  2. 状态转移方程

    • 如果小偷选择偷窃第 i 间房屋,那么他不能偷窃第 i-1 间房屋,因此他能获得的金额为 nums[i] + dp[i-2]
    • 如果小偷选择不偷窃第 i 间房屋,那么他能获得的金额就是 dp[i-1]
    • 所以,转移方程为:
      [
      dp[i] = \max(dp[i-1], nums[i] + dp[i-2])
      ]
  3. 边界条件

    • dp[0] = nums[0]:如果只有一间房屋,小偷只能偷窃这一间。
    • dp[1] = \max(nums[0], nums[1]):如果有两间房屋,小偷会选择偷窃金额更高的那一间。
  4. 最终结果dp[n-1],即为偷窃到最后一间房屋时的最大金额。

代码解析

接下来,我们逐行分析你提供的代码:

var rob = function (nums) {
    n = nums.length;
    function dfs(i) {
        if (i < 0) {
            return 0;
        }
        let res = Math.max(dfs(i - 1), dfs(i - 2) + nums[i]);
        return res;
    }
    return dfs(n - 1);
};

1. 函数定义

  • rob 函数接收一个数组 nums 作为输入,表示每间房屋内的现金。

2. 变量 n

n = nums.length;
  • n 代表房屋的数量。

3. 递归函数 dfs

function dfs(i) {
    if (i < 0) {
        return 0;
    }
    let res = Math.max(dfs(i - 1), dfs(i - 2) + nums[i]);
    return res;
}
  • 参数 i:当前考虑的房屋索引。
  • 递归终止条件:当 i < 0 时,返回 0,表示没有房屋可偷。
  • 状态转移
    • dfs(i - 1):不偷窃第 i 间房屋的情况下的最大金额。
    • dfs(i - 2) + nums[i]:偷窃第 i 间房屋的情况下的最大金额。
  • 返回值:返回这两种选择的最大值。

4. 开始递归

return dfs(n - 1);
  • 从最后一间房屋开始递归计算最大金额。

代码的不足之处

尽管上面的代码实现了基本的逻辑,但它的效率较低,因为它使用了递归且没有缓存中间结果,导致许多子问题被重复计算。这样会导致时间复杂度为 O(2^n),在房屋数量较多时,会非常慢。

动态规划优化

为了提高效率,我们可以用动态规划的方法实现这个问题,避免重复计算。下面是使用动态规划的改进代码:

var rob = function(nums) {
    const n = nums.length;
    if (n === 0) return 0;
    if (n === 1) return nums[0];

    const dp = new Array(n);
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);

    for (let i = 2; i < n; i++) {
        dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2]);
    }

    return dp[n - 1];
};

代码解析

  1. 边界条件

    • 如果没有房屋 (n === 0),返回 0。
    • 如果只有一间房屋 (n === 1),返回这一间的现金。
  2. 动态规划数组 dp

    • dp[0] 初始化为 nums[0]
    • dp[1] 初始化为 Math.max(nums[0], nums[1])
  3. 状态转移

    • 从第三间房屋(索引 2)开始,遍历每一间房屋,更新 dp[i] 为当前能够获得的最大金额。
  4. 返回结果

    • 返回 dp[n - 1],即为偷窃到最后一间房屋的最大金额。

时间与空间复杂度分析

  • 时间复杂度:O(n),因为我们只遍历了一次房屋列表。
  • 空间复杂度:O(n),用于存储动态规划数组。如果我们只关心前两个状态,可以进一步优化为 O(1) 的空间复杂度。

进一步优化(空间复杂度 O(1))

由于我们只需要前两个状态来计算当前状态,我们可以使用两个变量代替数组来保存状态。以下是进一步优化后的代码:

var rob = function(nums) {
    const n = nums.length;
    if (n === 0) return 0;
    if (n === 1) return nums[0];

    let prev1 = nums[0]; // dp[i-1]
    let prev2 = Math.max(nums[0], nums[1]); // dp[i]

    for (let i = 2; i < n; i++) {
        let current = Math.max(prev2, nums[i] + prev1);
        prev1 = prev2;
        prev2 = current;
    }

    return prev2;
};

在这个优化版本中,我们仅使用了两个变量 prev1prev2 来存储前两个状态,从而将空间复杂度降低到 O(1)。


在这里插入图片描述


问题概述

题目:给定一个非负整数数组 nums,表示每间房屋内的现金。小偷不能同时偷窃相邻的房屋。你的目标是计算小偷在不触发报警的情况下,能够偷窃到的最高金额。

示例

  • 输入:nums = [2, 7, 9, 3, 1]
  • 输出:12(偷窃房屋 1 和 3 的现金,将得到 2 + 9 + 1 = 12)

代码解析

下面是代码的逐行解析:

var rob = function(nums) {
    const n = nums.length;
    const memo = new Array(n).fill(-1); // -1 表示没有计算过

1. 初始化

  • n:房屋的数量。
  • memo:一个数组,用于存储已经计算过的结果,初始值为 -1,表示该索引还没有计算过。

2. 递归函数 dfs

    function dfs(i) {
        if (i < 0) { // 递归边界(没有房子)
            return 0;
        }
        if (memo[i] !== -1) { // 之前计算过
            return memo[i];
        }
        const res = Math.max(dfs(i - 1), dfs(i - 2) + nums[i]);
        memo[i] = res; // 记忆化:保存计算结果
        return res;
    }
  • 函数参数 i:表示当前考虑的房屋索引。

  • 递归边界

    • i < 0 时,表示没有房屋可偷窃,返回 0。
  • 记忆化检查

    • 如果 memo[i] 不等于 -1,说明之前已经计算过从 nums[0]nums[i] 的最大金额,直接返回 memo[i]
  • 状态转移

    • 计算偷窃的最大金额:
      • dfs(i - 1):不偷窃第 i 间房屋的情况下的最大金额。
      • dfs(i - 2) + nums[i]:偷窃第 i 间房屋的情况下的最大金额。
    • 选择这两种情况中的最大值,并将结果存储到 memo[i] 中。
  • 返回值:返回当前计算的结果 res

3. 开始递归

    return dfs(n - 1); // 从最后一个房子开始思考
};
  • 从最后一间房屋(索引为 n-1)开始递归计算能够偷窃的最大金额。

动态规划的思路

这段代码实际上实现了动态规划的递归版本,利用 记忆化(Memoization)来存储中间结果,避免重复计算。这样可以将时间复杂度从 O(2^n) 降低到 O(n),因为每个子问题只会被计算一次。

时间与空间复杂度分析

  • 时间复杂度:O(n),因为我们只计算了每个房屋的最大可偷金额一次。
  • 空间复杂度:O(n),用于存储 memo 数组以及递归调用栈的空间。

进一步优化(空间复杂度 O(1))

虽然当前的代码使用了记忆化来提高效率,但我们仍然可以进一步优化空间复杂度。我们只需要保存前两个状态,而不需要一个完整的数组。以下是优化后的代码:

var rob = function(nums) {
    const n = nums.length;
    if (n === 0) return 0;
    if (n === 1) return nums[0];

    let prev1 = 0; // dp[i-1]
    let prev2 = 0; // dp[i-2]

    for (let i = 0; i < n; i++) {
        let current = Math.max(prev2 + nums[i], prev1);
        prev2 = prev1;
        prev1 = current;
    }

    return prev1; // 返回最后一间房屋的最大金额
};

优化后的代码解析

  1. 边界条件

    • 如果没有房屋 (n === 0),返回 0。
    • 如果只有一间房屋 (n === 1),返回该房屋的现金。
  2. 状态变量

    • prev1 表示偷窃到 i-1 间房屋时的最大金额。
    • prev2 表示偷窃到 i-2 间房屋时的最大金额。
  3. 状态转移

    • 遍历每间房屋,计算当前房屋的最大可偷金额。
    • 更新 prev1prev2
  4. 返回结果

    • 返回 prev1,即为偷窃到最后一间房屋时的最大金额。

在这里插入图片描述
在你提供的代码片段中,我们使用动态规划来解决“打家劫舍”问题。你提到的疑问主要集中在更新动态规划数组 f 的下标时,为什么我们只将 f 的下标加 2,而 nums 的下标不需要加 2。让我们来详细分析这个问题。

代码分析

首先,我们来看一下代码的结构:

var rob = function(nums) {
    const n = nums.length;
    const f = new Array(n + 2).fill(0); // 创建长度为 n + 2 的数组 f,并初始化为 0
    for (let i = 0; i < n; i++) {
        f[i + 2] = Math.max(f[i + 1], f[i] + nums[i]); // 更新 f 数组
    }
    return f[n + 1]; // 返回结果
};

1. 动态规划数组 f 的含义

  • f[i] 表示偷窃到第 i 间房屋时能够获得的最大金额。
  • 为了处理边界情况,我们将 f 数组的长度设置为 n + 2,这样可以避免在计算时出现下标越界的问题。

2. 为什么 f 的下标加 2

在动态规划的转移方程中,我们需要考虑当前房屋 i 的值以及之前的房屋的值。具体来说,f[i + 2] 代表的是在偷窃到房屋 i 后的状态。我们需要比较两种情况:

  • 不偷窃第 i 间房屋:此时的最大金额为 f[i + 1]
  • 偷窃第 i 间房屋:此时的最大金额为 f[i] + nums[i]

3. nums 的下标为什么不需要加 2

  • 在循环中,i 的范围是从 0n - 1,这正好对应 nums 数组的有效下标。
  • 在更新 f[i + 2] 时,我们是在 f 数组中填充结果,而 nums[i] 仍然是对原始数组 nums 的直接访问。
  • 具体来说:
    • i = 0 时,f[2] 计算的是 Math.max(f[1], f[0] + nums[0]),这里的 nums[0] 是直接使用的。
    • i = 1 时,f[3] 计算的是 Math.max(f[2], f[1] + nums[1]),这里的 nums[1] 也是直接使用的。

因此,我们并不需要对 nums 的下标进行加 2 的操作,因为我们在循环中是按照原始的下标访问 nums 的。

4. 处理边界情况

  • 由于我们在 f 数组的长度上加了 2,我们可以安全地访问 f[n + 1] 而不会越界。
  • 这样做的好处是,我们可以在实现中避免复杂的边界条件处理,使代码更加简洁和易懂。

在这里插入图片描述


问题概述

题目:给定一个非负整数数组 nums,表示每间房屋内的现金。小偷不能同时偷窃相邻的房屋。你的目标是计算小偷在不触发报警的情况下,能够偷窃到的最高金额。

示例

  • 输入:nums = [2, 7, 9, 3, 1]
  • 输出:12(偷窃房屋 1 和 3 的现金,将得到 2 + 9 + 1 = 12)

代码解析

下面是代码的逐行解析:

var rob = function (nums) {
  let n = nums.length;
  let f0 = 0,
    f1 = 0;

1. 初始化

  • n:房屋的数量。
  • f0:表示偷窃到第 i-2 间房屋时的最大金额(即不偷窃第 i-1 间房屋的情况下的最大金额)。
  • f1:表示偷窃到第 i-1 间房屋时的最大金额。

在初始状态下,f0f1 都被设置为 0,表示在没有偷窃任何房屋的情况下,最大金额为 0

2. 遍历房屋

  for (let i = 0; i < n; i++) {
    let newF = Math.max(f1, f0 + nums[i]);
  • 使用一个循环遍历所有的房屋。
  • newF 计算当前房屋的最大可偷金额:
    • f1:不偷窃当前房屋 i 的情况下的最大金额。
    • f0 + nums[i]:偷窃当前房屋 i 的情况下的最大金额。这里的 f0 是指在偷窃当前房屋之前的最大金额。

3. 更新状态

    f0 = f1;
    f1 = newF;
  }
  • 更新 f0f1
    • f0 更新为之前的 f1,表示在下一个循环中,f0 成为新的 f1
    • f1 更新为 newF,即当前房屋的最大可偷金额。

4. 返回结果

  return f1;
};
  • 最终返回 f1,即为偷窃到最后一间房屋时的最大金额。

动态规划的思路

在这个解决方案中,我们使用动态规划来优化问题的求解。具体来说:

  1. 状态定义

    • f0f1 分别表示在不偷窃当前房屋和偷窃当前房屋时的最大金额。
  2. 状态转移方程

    • 在遍历每个房屋时,我们需要决定是偷窃当前房屋还是不偷窃。通过 Math.max(f1, f0 + nums[i]) 来选择最大金额。
  3. 空间优化

    • 通过只使用两个变量 f0f1,我们将空间复杂度从 O(n) 降低到 O(1),因为我们只关心前两个状态的值。

时间与空间复杂度分析

  • 时间复杂度:O(n),因为我们遍历了一次房屋列表。
  • 空间复杂度:O(1),只使用了常数个额外的变量。

进一步理解

通过这个代码,我们可以更深入地理解以下几个重要的算法概念:

  • 动态规划的优化:在很多动态规划问题中,往往只需要前几个状态的信息。通过适当的状态定义和优化,可以显著减少空间复杂度。

  • 状态转移的选择:在动态规划中,如何选择状态转移的方式是关键。这里通过比较偷与不偷的情况来决定当前的最优解。

  • 递推关系的理解:理解动态规划中的递推关系是解决问题的核心。通过将问题拆解为子问题,我们可以逐步构建出最终的解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值