返回

前端刷题路-Day85:打家劫舍II

前端

在算法的世界里,动态规划如同一位经验丰富的工匠,它擅长将复杂的问题拆解成一个个小的、可重复解决的子问题,最终巧妙地将这些子问题的答案组合起来,得到整个问题的最优解。今天,我们就来探索一道经典的动态规划题目——“打家劫舍 II”,看看如何利用动态规划的思想,帮助一位“专业”的小偷,在不触发警报的前提下,最大化他的收益。

假设你是一位技艺高超的小偷,你盯上了一排沿街的房屋,每间房屋都藏着数量不等的现金。这些房屋排列成一个环形,也就是说,第一间房屋和最后一间房屋是相邻的。为了防止被抓,你不能连续偷窃相邻的两间房屋,否则会触发报警系统。现在,你的目标是制定一个偷窃计划,在不触发警报的情况下,偷到最多的现金。

面对这个问题,我们首先想到的是,如果房屋不是环形排列的,而是一条直线,那么问题就简单多了。我们可以用动态规划来解决:用一个数组 dp 来记录偷窃到每间房屋时所能获得的最大收益。dp[i] 表示偷窃到第 i 间房屋时的最大收益。显然,dp[0] 等于第一间房屋的现金数量。对于 dp[1],我们需要比较偷窃第一间房屋和第二间房屋哪个收益更大。对于 dp[i] (i > 1),我们有两个选择:要么偷窃第 i 间房屋,那么就不能偷窃第 i-1 间房屋,收益为 dp[i-2] + nums[i];要么不偷窃第 i 间房屋,那么收益就等于 dp[i-1]。我们选择两者中收益更大的那个。

但是,现在房屋是环形排列的,这就意味着第一间房屋和最后一间房屋不能同时被偷窃。为了解决这个问题,我们可以将问题分解成两个子问题:

  1. 不偷窃最后一间房屋: 这就相当于把环形房屋变成了一条直线,我们可以用上面提到的动态规划方法来解决。
  2. 不偷窃第一间房屋: 这也相当于把环形房屋变成了一条直线,我们同样可以用动态规划方法来解决。

最后,我们比较这两个子问题的结果,取其中较大的那个,就是最终的答案,也就是在环形房屋中能够偷窃到的最大现金数量。

下面,我们用 JavaScript 代码来实现这个思路:

function rob(nums) {
  const n = nums.length;
  if (n === 0) return 0;
  if (n === 1) return nums[0];
  if (n === 2) return Math.max(nums[0], nums[1]);

  // 不偷窃最后一间房屋
  const dp1 = new Array(n - 1).fill(0);
  dp1[0] = nums[0];
  dp1[1] = Math.max(nums[0], nums[1]);
  for (let i = 2; i < n - 1; i++) {
    dp1[i] = Math.max(dp1[i - 1], dp1[i - 2] + nums[i]);
  }

  // 不偷窃第一间房屋
  const dp2 = new Array(n - 1).fill(0);
  dp2[0] = nums[1];
  dp2[1] = Math.max(nums[1], nums[2]);
  for (let i = 2; i < n - 1; i++) {
    dp2[i] = Math.max(dp2[i - 1], dp2[i - 2] + nums[i + 1]);
  }

  return Math.max(dp1[n - 2], dp2[n - 2]);
}

这段代码的时间复杂度是 O(n),因为我们需要遍历两次数组 nums。空间复杂度也是 O(n),因为我们需要两个长度为 n-1 的数组 dp1dp2 来存储中间结果。

常见问题解答

  1. 为什么需要将问题分解成两个子问题? 因为环形房屋的特殊性,第一间房屋和最后一间房屋不能同时被偷窃。为了处理这种情况,我们需要分别考虑不偷窃第一间房屋和不偷窃最后一间房屋两种情况,然后取两者中收益更大的那个。

  2. 动态规划数组 dp 的含义是什么? dp[i] 表示偷窃到第 i 间房屋时所能获得的最大收益。

  3. 状态转移方程是如何推导出来的? 对于 dp[i],我们有两个选择:要么偷窃第 i 间房屋,那么就不能偷窃第 i-1 间房屋,收益为 dp[i-2] + nums[i];要么不偷窃第 i 间房屋,那么收益就等于 dp[i-1]。我们选择两者中收益更大的那个,这就是状态转移方程的由来。

  4. 代码中为什么要单独处理房屋数量为 0、1、2 的情况? 这是为了避免数组越界错误。当房屋数量为 0 或 1 时,可以直接返回结果;当房屋数量为 2 时,只需要比较两间房屋的现金数量即可。

  5. 除了动态规划,还有其他方法可以解决这个问题吗? 是的,还可以用递归的方法来解决,但是递归方法的时间复杂度较高,效率不如动态规划。

通过这道“打家劫舍 II”的题目,我们看到了动态规划的强大之处。它能够将看似复杂的问题分解成一个个简单的子问题,并通过状态转移方程将这些子问题的解联系起来,最终得到整个问题的最优解。在实际应用中,动态规划被广泛应用于各种优化问题,例如路径规划、资源分配、背包问题等等。掌握动态规划的思想,对于解决实际问题具有重要的意义。