题目:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
解法一(动态规划):
首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋,可以偷窃到最高总金额。如果只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。
注意到当房屋数量不超过两间时,最多只能偷窃一间房屋,因此不需要考虑首尾相连的问题。如果房屋数量大于两间,就必须考虑首尾相连的问题,第一间房屋和最后一间房屋不能同时偷窃。如果偷窃了第一间房屋,则不能偷窃最后一间房屋,因此偷窃房屋的范围是第一间房屋到最后第二间房屋;如果偷窃了最后一间房屋,则不能偷窃第一间房屋,因此偷窃房屋的范围是第二间房屋到最后一间房屋。
假设数组 nums 的长度为 n。如果不偷窃最后一间房屋,则偷窃房屋的下标范围是 [0,n−2];如果不偷窃第一间房屋,则偷窃房屋的下标范围是 [1,n−1]。对于两段下标范围分别计算可以偷窃到的最高总金额,其中的最大值即为在 n 间房屋中可以偷窃到的最高总金额。
假设偷窃房屋的下标范围是 [start,end],用 dp[i] 表示在下标范围 [start,i] 内可以偷窃到的最高总金额,那么就有如下的状态转移方程:
边界条件为:
,只有一间房,则偷窃该房屋
,只有两间房,偷窃金额较高的房屋
计算得到dp[end]即为下标范围[start, end]内可以偷窃到的最高总金额。分别取(start,end)=(0, n-2)和(start, end)=(1, n-1)进行计算,取两个dp[end]中的最大值,即可得到最终结果。
根据上述思路,可以得到时间复杂度 O(n) 和空间复杂度 O(n) 的实现。考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关,因此可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额,将空间复杂度降到 O(1)。如下为实现代码:
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));
}
};
笔者小记:
本题为经典的贪心算法题目,可以使用贪心算法的思想或者动态规划的思想来解决,考虑当前偷窃到当前房屋所得金额的最大值【max(t)】仅和当前房屋的金额以及偷窃到当前房屋的前一个房屋所得最大值金额【max(t-1)】,以及偷窃到当前房屋的前第二个房屋所得最大值金额【max(t-2)】有关。因此更大规模的最优解仅与前两个子问题的最优解有关系,所以不难构造动态转移方程。
在利用动态规划算法进行解题时,第一要考虑动态转移方程(状态转移方程:找出 dp[i]
和 dp[i-1]
、dp[i-2]
等之间的关系),第二要考虑边界条件(即指的是在递归或迭代过程中,不需要再拆分的问题,即可以直接得到结果的情况。边界条件的设置对于正确构建 DP 递推公式至关重要,边界条件是初始化 DP 数组:为后续状态转移提供基础。提供直接解:有些问题在初始状态下答案已知,无需进一步计算)。除此之外,动态规划/贪心算法思想一般都会先分成两类,并取两个最后大规模问题最优解之间的最大值,作为本题最大值问题(在本题中,即为分别取(start,end)=(0, n-2)和(start, end)=(1, n-1)进行计算,取两个dp[end]中的最大值,即可得到最终结果)。
一个完整的动态规划解法通常包括以下几个步骤:
- 定义状态(
dp[i]
或dp[i][j]
):描述子问题的解。 - 边界条件(Base Case):确定最小子问题的解。
- 状态转移方程:找出
dp[i]
和dp[i-1]
、dp[i-2]
等之间的关系。