0213-打家劫舍 II

Raphael Liu Lv10

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈
,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统, 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

**输入:** nums = [2,3,2]
**输出:** 3
**解释:** 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

**输入:** nums = [1,2,3,1]
**输出:** 4
**解释:** 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

**输入:** nums = [1,2,3]
**输出:** 3

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

前言

这道题是「198. 打家劫舍 」的进阶,和第 198 题的不同之处是,这道题中的房屋是首尾相连的,第一间房屋和最后一间房屋相邻,因此第一间房屋和最后一间房屋不能在同一晚上偷窃。

和第 198 题相似,这道题也可以使用动态规划解决。建议读者首先阅读「198. 打家劫舍的官方题解 」,了解动态规划的思想。

方法一:动态规划

首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋,可以偷窃到最高总金额。如果只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。

注意到当房屋数量不超过两间时,最多只能偷窃一间房屋,因此不需要考虑首尾相连的问题。如果房屋数量大于两间,就必须考虑首尾相连的问题,第一间房屋和最后一间房屋不能同时偷窃。

如何才能保证第一间房屋和最后一间房屋不同时偷窃呢?如果偷窃了第一间房屋,则不能偷窃最后一间房屋,因此偷窃房屋的范围是第一间房屋到最后第二间房屋;如果偷窃了最后一间房屋,则不能偷窃第一间房屋,因此偷窃房屋的范围是第二间房屋到最后一间房屋。

假设数组 $\textit{nums}$ 的长度为 $n$。如果不偷窃最后一间房屋,则偷窃房屋的下标范围是 $[0, n-2]$;如果不偷窃第一间房屋,则偷窃房屋的下标范围是 $[1, n-1]$。在确定偷窃房屋的下标范围之后,即可用第 198 题的方法解决。对于两段下标范围分别计算可以偷窃到的最高总金额,其中的最大值即为在 $n$ 间房屋中可以偷窃到的最高总金额。

假设偷窃房屋的下标范围是 $[\textit{start},\textit{end}]$,用 $\textit{dp}[i]$ 表示在下标范围 $[\textit{start},i]$ 内可以偷窃到的最高总金额,那么就有如下的状态转移方程:

$$
\textit{dp}[i] = \max(\textit{dp}[i-2]+\textit{nums}[i], \textit{dp}[i-1])
$$

边界条件为:

$$
\begin{cases}
\textit{dp}[\textit{start}] = \textit{nums}[\textit{start}] & 只有一间房屋,则偷窃该房屋 \
\textit{dp}[\textit{start}+1] = \max(\textit{nums}[\textit{start}], \textit{nums}[\textit{start}+1]) & 只有两间房屋,偷窃其中金额较高的房屋
\end{cases}
$$

计算得到 $\textit{dp}[\textit{end}]$ 即为下标范围 $[\textit{start},\textit{end}]$ 内可以偷窃到的最高总金额。

分别取 $(\textit{start},\textit{end})=(0,n-2)$ 和 $(\textit{start},\textit{end})=(1,n-1)$ 进行计算,取两个 $\textit{dp}[\textit{end}]$ 中的最大值,即可得到最终结果。

根据上述思路,可以得到时间复杂度 $O(n)$ 和空间复杂度 $O(n)$ 的实现。考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额,将空间复杂度降到 $O(1)$。

<fig1,fig2,fig3,fig4,fig5,fig6,fig7,fig8,fig9>

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
} else if (length == 2) {
return Math.max(nums[0], nums[1]);
}
return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}

public int robRange(int[] nums, int start, int end) {
int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var rob = function(nums) {
const length = nums.length;
if (length === 1) {
return nums[0];
} else if (length === 2) {
return Math.max(nums[0], nums[1]);
}
return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
};

const robRange = (nums, start, end) => {
let first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for (let i = start + 2; i <= end; i++) {
const temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func _rob(nums []int) int {
first, second := nums[0], max(nums[0], nums[1])
for _, v := range nums[2:] {
first, second = second, max(first+v, second)
}
return second
}

func rob(nums []int) int {
n := len(nums)
if n == 1 {
return nums[0]
}
if n == 2 {
return max(nums[0], nums[1])
}
return max(_rob(nums[:n-1]), _rob(nums[1:]))
}

func max(a, b int) int {
if a > b {
return a
}
return b
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution:
def rob(self, nums: List[int]) -> int:
def robRange(start: int, end: int) -> int:
first = nums[start]
second = max(nums[start], nums[start + 1])
for i in range(start + 2, end + 1):
first, second = second, max(first + nums[i], second)
return second

length = len(nums)
if length == 1:
return nums[0]
elif length == 2:
return max(nums[0], nums[1])
else:
return max(robRange(0, length - 2), robRange(1, length - 1))
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int robRange(vector<int>& nums, int start, int end) {
int first = nums[start], second = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = max(first + nums[i], second);
first = temp;
}
return second;
}

int rob(vector<int>& nums) {
int length = nums.size();
if (length == 1) {
return nums[0];
} else if (length == 2) {
return max(nums[0], nums[1]);
}
return max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
};
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int robRange(int* nums, int start, int end) {
int first = nums[start], second = fmax(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = fmax(first + nums[i], second);
first = temp;
}
return second;
}

int rob(int* nums, int numsSize) {
if (numsSize == 1) {
return nums[0];
} else if (numsSize == 2) {
return fmax(nums[0], nums[1]);
}
return fmax(robRange(nums, 0, numsSize - 2), robRange(nums, 1, numsSize - 1));
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组长度。需要对数组遍历两次,计算可以偷窃到的最高总金额。

  • 空间复杂度:$O(1)$。

 Comments
On this page
0213-打家劫舍 II