返回

打家劫舍:运用动态规划的巧妙策略

前端

算法视野下的“打家劫舍”:运用动态规划的妙方

在算法的领域中,动态规划是一种强大的技术,它能将复杂问题分解成较小的子问题,从而逐步求解。今天,我们就将聚焦于一个经典的动态规划问题——“打家劫舍”。

问题陈述:

假设你是一个窃贼,准备对一条街上的房屋进行抢劫。这些房屋成一排排列,每个房屋中都存放着一定数量的钱财。但是,有一个限制:相邻的房屋设有报警系统,一旦触发就会惊动警方。因此,你无法抢劫相邻的两间房屋。

你的目标是在不触发报警系统的情况下,抢劫尽可能多的钱财。

递归解法:

初探这个问题,我们可能首先想到递归解法。基本思路是分别计算抢劫第 i 间房屋和不抢劫第 i 间房屋所能获得的最大收益,然后取两者中的较大值作为第 i 间房屋的收益。

function rob(nums) {
  if (nums === null || nums.length === 0) {
    return 0;
  }

  // 初始化
  const n = nums.length;
  const dp = new Array(n).fill(-1);

  // 求解
  return Math.max(robHelper(nums, n - 1, dp), robHelper(nums, n - 2, dp));
}

function robHelper(nums, i, dp) {
  // 备忘录优化
  if (dp[i] !== -1) {
    return dp[i];
  }

  // 边界条件
  if (i < 0) {
    return 0;
  }

  // 状态转移方程
  dp[i] = Math.max(robHelper(nums, i - 2, dp) + nums[i], robHelper(nums, i - 1, dp));

  return dp[i];
}

自顶向下备忘录法:

递归解法的效率并不理想,因为在求解过程中会重复计算相同的子问题。为了优化效率,我们可以采用自顶向下备忘录法。通过维护一个备忘录表 dp 来记录已经计算过的子问题的解,避免重复计算。

自底向上动态规划:

自顶向下的备忘录法虽然改善了递归解法的效率,但仍然需要遍历所有可能的子问题。为了进一步优化,我们可以采用自底向上的动态规划方法。这种方法从最小的子问题开始求解,逐步累积求解更大的子问题,最终得到整体问题的解。

function rob(nums) {
  if (nums === null || nums.length === 0) {
    return 0;
  }

  // 初始化
  const n = nums.length;
  const dp = new Array(n + 1).fill(0);

  // 自底向上求解
  for (let i = 1; i <= n; i++) {
    // 状态转移方程
    dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
  }

  // 返回最大收益
  return dp[n];
}

结论:

“打家劫舍”问题是一个经典的动态规划问题,通过递归、自顶向下备忘录法和自底向上动态规划这三种方法,我们可以逐步优化求解效率。动态规划是一种重要的算法技术,在求解复杂问题时具有广泛的应用场景。