213. 打家劫舍 II (House Robber II)

4,054 阅读3分钟

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

213. 打家劫舍2 题目描述:你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组 nums[i]nums[i],计算在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例1示例2
输入nums=[2,3,2]nums = [2,3,2]
输出33
解释:你不能先偷窃 00 号房屋(金额 = 22),然后偷窃 22 号房屋(金额 = 22), 因为他们是相邻的。
输入nums=[1,2,3,1]nums = [1,2,3,1]
输出44
解释:你可以先偷窃 00 号房屋(金额 = 11),然后偷窃 22 号房屋(金额 = 33)。偷窃到的最高金额 = 1+3=41 + 3 = 4

中规中矩的动态规划

本题比 198. 打家劫舍 仅仅多了一个条件,就是数据的首尾元素不能同时强。其实这是一个“烟雾弹”,首尾不能同时抢翻译过来就是,先计算 [0,n2][0,n-2] 房屋区间内的最大金额,再计算 [1,n1][1,n-1] 房屋区间内的最大金额,最后比较哪个多。

1、确定 dp 状态数组

定义 dp[i]dp[i] 为前 ii 房间内所能抢劫的最大金额。其中,i[0,n1),n=nums.lengthi \in [0,n-1),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 的情况下。

hold on!!! 收尾不能同时抢的逻辑体现在哪里?就在 初始状态遍历顺序 中体现。

3、确定 dp 初始状态

抢第 00 个房间和第 n1n-1 个房间,总得放弃一个。因此我们定义两个参数 startstartendend,范围[start,end)[start,end),其中 end>2end > 2,故初始值为

  • dp[start]=nums[start]dp[start] = nums[start]

  • dp[start+1]=max(nums[start],nums[start+1])dp[start + 1] = max(nums[start],nums[start + 1])

4、确定遍历顺序

  • 抢首不抢尾,start=0,end=n1start=0,end=n-1,遍历范围 [1,n1)[1,n-1)

  • 抢尾不抢首,start=1,end=nstart=1,end=n,遍历范围 [2,n)[2,n)

5、确定最终返回值

回归状态定义,最终返回值为 max(dp1[n2],dp2[n2])max(dp_1[n-2], dp_2[n - 2])

6、代码示例

当房间个数小于等于 33 时,必定两两相连,所以三个房间选择金额最大的即可。

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

    if(n < 3) return Math.max(...nums);

    const dp1 = new Array(n - 1).fill(0); // 偷第0个不偷len-1个,遍历[0, len -1)
    dp1[0] = nums[0];
    dp1[1] = Math.max(nums[0], nums[1]);

    const dp2 = new Array(n - 1).fill(0);// 不偷第0个偷len-1个,遍历[1, len)
    dp2[0] = nums[1];
    dp2[1] = Math.max(nums[1], nums[2]);

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

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

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

重复遍历逻辑提取 robInRange

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

    if(n < 3) return Math.max(...nums);


    return Math.max(robInRange(nums, 0, n - 1), robInRange(nums, 1, n));
}

/**
 * 范围是[start,end),返回能抢到的最大值
 */
function robInRange(nums: number[], start: number, end: number): number {
    const dp = new Array(end - start).fill(0);
    dp[start] = nums[start];
    dp[start + 1] = Math.max(nums[start], nums[start + 1]);

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

    return dp[end - 1];
}

状态压缩

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

    if(n < 3) return Math.max(...nums);

    return Math.max(robInRange(nums, 0, n - 1), robInRange(nums, 1, n));
}

/**
 * 范围是[start,end),返回能抢到的最大值
 */
function robInRange(nums: number[], start: number, end: number): number {
    let first = nums[start];
    let second = Math.max(nums[start], nums[start + 1]);

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

    return second;
}