198. 打家劫舍 (House Robber)

3,968 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

198. 打家劫舍 题目描述:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例1示例2
输入[1,2,3,1][1,2,3,1]
输出44
解释: 偷窃 00 号房屋 (金额 = 11) ,然后偷窃 22 号房屋 (金额 = 33)。偷窃到的最高金额为 1+3=41 + 3 = 4
输入[2,7,9,3,1][2,7,9,3,1]
输出1212
解释: 偷窃 00 号房屋 (金额 = 22), 偷窃 22 号房屋 (金额 = 99),接着偷窃 44 号房屋 (金额 = 11)。偷窃到的最高金额为 2+9+1=122 + 9 + 1 = 12

为何能用动归来解决?动归的充要条件:

  1. 最值问题? 求第 nn 天的最大金额,n=nums.length1n=nums.length-1;
  2. 无后效行? 没有要求指出具体抢劫哪些房子的金额最大;
  3. 最值可以穷举? 只要不怕麻烦,肯定能把抢劫的所用情况枚举出来,并比较金额;
  4. 最优子结构? 如果我们知道抢劫前 i1i-1 间房子的最大金额,以及第 i1i-1 间的抢劫状态,一定能知道抢劫前 ii 间房子的最大金额。

中规中矩的动态规划

1、确定 dp 状态数组

对于每个房间,只有两种偷窃状态,偷和不偷,所以定义 dp[i][j]dp[i][j] 为从 [0,i][0, i] 房间中偷窃,且第 ii 个房子的偷窃状态为 jj 时获取的最大金额,其中,

  • i[0,n)i \in [0, n)nnnumsnums 数组的长度

  • j=0,1j=0,100 代表不偷窃,11 代表偷窃

2、确定 dp 状态转移方程

dp[i][0]dp[i][0] 代表第 ii 个房间不偷窃时的最大金额,因为第 ii 个房间选择不偷窃,所以第 i1i-1 个房间偷与不偷均可,故其值与两个状态有关系

  • dp[i1][0]dp[i-1][0] 代表第 i1i - 1 个房间不偷窃时的最大金额

  • dp[i1][1]dp[i-1][1] 代表第 i1i - 1 个房间偷窃时的最大金额

dp[i][0]=max(dp[i1][0],dp[i1][1])dp[i][0] = max(dp[i-1][0], dp[i-1][1])

dp[i][1]dp[i][1] 代表第 ii 个房间偷窃时的最大金额,因为第 ii 个房间选择偷窃,所以第 i1i-1 个房间一定不能偷窃。故,dp[i][1]dp[i][1] 仅与一个状态有关系

  • dp[i1][0]dp[i-1][0] 代表第 i1i-1 个房间不偷窃时的最大金额,其值应加上当前房屋的金额 nums[i]nums[i]
dp[i][1]=dp[i1][0]+nums[i]dp[i][1] = dp[i-1][0] + nums[i]

3、确定 dp 初始状态

  • dp[0][0]=0dp[0][0] = 0,第 00 个房子选择 不偷窃 时的最大金额;

  • dp[0][1]=nums[i]dp[0][1] = nums[i],第 00 个房子选择 偷窃 时的最大金额。

4、确定遍历顺序

从第 11 个房子一直遍历到第 n1n-1 个房子

5、确定最终返回值

回归到状态的定义中

  • dp[n1][0]dp[n-1][0] 为从 [0,n1][0, n-1] 房间中偷窃,且第 n1n-1 个房子选择 不偷窃 时获取的最大金额

  • dp[n1][1]dp[n-1][1] 为从 [0,n1][0, n-1] 房间中偷窃,且第 n1n-1 个房子选择 偷窃 时获取的最大金额

故最终返回值 max(dp[n1][0],dp[n1][1])max(dp[n-1][0], dp[n-1][1])

6、代码示例

/**
 * 空间复杂度 O(n),n是nums数组的长度
 * 时间复杂度 O(n)
 */
function rob(nums: number[]): number {
    const n = nums.length;

    if(n < 2) return nums[0];

    const dp = new Array(n).fill(0).map(() => [0, 0]);
    dp[0][0] = 0;
    dp[0][1] = nums[0];

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

    return Math.max(dp[n - 1][0], dp[n-1][1]);
}

状态压缩: 因为第 ii 个房间的最大金额仅与第 i1i-1 个房间的最大金额有关,故可压缩空间复杂度为常量。

/**
* 空间复杂度 O(1)
* 时间复杂度 O(n),n是nums数组的长度
*/
function rob(nums: number[]): number {
    const n = nums.length;

    if(n < 2) return nums[0];

    // passed 不偷,stolen 偷
    let passed = 0, stolen = nums[0];

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

    return Math.max(passed, stolen);
}

另一种动态规划思路

1、确定 dp 状态数组

既然题目要求计算 nn 个房间的最大金额,不妨定义 dp[i]dp[i] 为从 [0,i][0,i] 区间的房间内所能抢劫的最大金额。其中,i[0,n),n=nums.lengthi \in [0,n),n = nums.length

2、确定 dp 状态转移方程

如果房间数量大于两间,如何获取最高金额?对于第 i(i2)i(i \ge 2)间房子,有两种方式:

  • 偷窃 ii 间房屋,那就不能偷窃第 i1i-1 间房屋,最大总金额为前 i2i - 2 个房屋的最大金额 dp[i2]dp[i-2] 加上第 ii 间房屋的金额 nums[i]nums[i],即 dp[i2]+nums[i]dp[i-2] + nums[i]

  • 不偷窃第 ii 间房屋,最大总金额为前 i1i-1 个房屋的最大金额,即 dp[i1]dp[i-1]

故,dp[i]=max(dp[i2]+nums[i],dp[i1])dp[i]= max(dp[i-2]+nums[i], dp[i-1]),当 i2i \ge 2 的情况下。

3、确定 dp 初始状态

i2i \ge 2 时的状态方程为 dp[i]=max(dp[i2]+nums[i],dp[i1])dp[i]= max(dp[i-2]+nums[i], dp[i-1]),所以要初始化 i=0,1i=0,1 时的状态。

  • 当只有一间房子时,放心拿钱就好,dp[0]=nums[0]dp[0]=nums[0]

  • 当有两间房子时,只能选择其中一件房子偷,选择金额最大的即可,dp[1]=max(nums[0],nums[1])dp[1]=max(nums[0],nums[1])

4、确定遍历顺序

i=2i=2 一直遍历到 i=n1i=n-1

5、确定最终返回值

回归状态定义,从 [0,n1][0,n-1] 区间的房间内所能抢劫的最大金额即为 dp[n1]dp[n-1]

6、代码示例

/**
 * 空间复杂度 O(n)
 * 时间复杂度 O(n),n是nums数组的长度
 */
function rob(nums: number[]): number {
    const n = nums.length;

    if(n === 1) return nums[0];

    const dp = new Array(n).fill(0);
    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 - 2] + nums[i], dp[i - 1])
    }

    return dp[n - 1];
}

状态压缩

/**
 * 空间复杂度 O(1)
 * 时间复杂度 O(n),n是nums数组的长度
 */
function rob(nums: number[]): number {
    const n = nums.length;

    if(n === 1) return nums[0];

    let first = nums[0], second = Math.max(nums[0], nums[1]);

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

    return second;
}