目录
[面试题 89 : 房屋偷盗](#面试题 89 : 房屋偷盗)
[面试题 90 : 环形房屋偷盗](#面试题 90 : 环形房屋偷盗)
面试题 89 : 房屋偷盗
题目:
输入一个数组表示某条街道上的一排房屋内财产的数目。如果这条街道上相邻的两幢房屋被盗就会自动触发报警系统 。请计算小偷在这条街道上最多能偷取到多少财产。例如,街道上 5 幢房屋内的财产用数组 [2, 3, 4, 5, 3] 表示,如果小偷到下标为 0、2 和 4 的房屋内盗窃,那么他能偷取到价值为 9 的财产,这是他在不触发报警系统的情况下能偷取到的最多的财物,如下图所示被盗的房屋上方用特殊符号标出。
分析:
小偷一次只能进入一幢房屋内盗窃,因此到街道上所有房屋中盗窃需要多个步骤,每一步到一幢房屋内盗窃。由于这条街道有报警系统,因此他每到一幢房屋前都面临一个选择,考虑是不是能进去偷东西。完成一件事情需要多个步骤,并且每一步都面临多个选择,这看起来是一个适合运用回溯法的问题。但由于这个问题并没有要求列举出小偷所有满足条件的偷盗的方法,而只是求最多能偷取的财物的数量,也就是求问题的最优解,因此这个问题适合运用动态规划。
分析确定状态转移方程:
应用动态规划解决问题的关键在于找出状态转移方程。这个问题的输入是一个用数组表示的一排房屋内的财物数量,这个数组就是一个序列。用动态规划解决单序列问题的关键在于找到序列中一个元素对应的解和前面若干元素对应的解的关系,并用状态转移方程表示。
假设街道上有 n 幢房屋(分别用 0 ~ n - 1 标号),小偷从标号为 0 的房屋开始偷东西。可以用 f(i) 表示小偷从标号为 0 的房屋开始到标号为 i 的房屋为止最多能偷取到的财物的数量。f(n - 1) 就是小偷从 n 幢房屋中能偷取到的最多财物的数量,即问题的解。
小偷在标号为 i 的房屋前有两个选择:
-
一个选择是他进去偷东西。由于街道上有报警系统,因此,他不能进入相邻的标号为 i - 1 的房屋内偷东西,之前他最多能偷取到的财物的数量是 f(i - 2)。因此,小偷如果进入标号为 i 的房屋并盗窃,他最多能偷得 f(i - 2) + nums[i](nums 是表示房屋内财物数量的数组)。
-
另一个选择是小偷进入标号为 i 的房屋,那么他可以进入标号为 i - 1 的房屋内偷东西,因此此时他最多能偷取的财物的数量为 f(i - 1)。
那么小偷在到达标号为 i 的房屋时,他能偷取的财物的最大值就是两个选项的最大值,即 f(i) = max(f(i - 2) + nums[i], f(i - 1)),这就是解决这个问题的状态转移方程。
上述状态转移方程有一个隐含条件,假设 i 大于或等于 2。当 i 等于 0 时,f(0) = nums[0];当 i 等于 1 时,f(1) = max(nums[0], nums[1])。
带缓存的递归代码:
状态转移方程是一个递归的表达式,很容易将它转换成递归函数,只是要避免不必要的重复计算。可以创建一个数组 dp,它的第 i 个元素 dp[i] 用来保存 f(i) 的结果。如果 f(i) 之前已经计算出结果,那么只需要从数组 dp 中读取 dp[i] 的值,不再重复计算。如果之前从来没有计算过,则根据状态转移方程递归计算。
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, -1);
helper(nums, dp, n - 1);
return dp[n - 1];
}
private:
void helper(vector<int>& nums, vector<int>& dp, int i) {
if (i == 0)
{
dp[i] = nums[0];
}
else if (i == 1)
{
dp[i] = max(nums[0], nums[1]);
}
else if (dp[i] == -1)
{
helper(nums, dp, i - 1);
helper(nums, dp, i - 2);
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
}
};
函数 helper 其实是将状态转移方程 f(i) = max(f(i - 2) + nums[i], f(i - 1)) 翻译成 C++ 语言的代码。由于状态转移方程要求 i 大于或等于 2,因此函数 helper 还单独处理了 i 分别等于 0 和 1 的这两种特殊情况。
上述代码由于能确保每个 f(i) 只需要计算一次,因此时间复杂度是 O(n)。由于需要一个长度为 n 的数组,因此空间复杂度也是 O(n)。
空间复杂度为 O(n) 的迭代代码:
也可以换一种思路,即先求出 f(0) 和 f(1) 的值,然后用 f(0) 和 f(1) 的值求出 f(2),用 f(1) 和 f(2) 的值求出 f(3),以此类推,直至求出 f(n - 1)。这种自下而上的思路通常可以用一个 for 循环实现。
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n);
dp[0] = nums[0];
if (n > 1)
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < n; ++i)
{
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[n - 1];
}
};
显然,上述代码的时间复杂度和空间复杂度都是 O(n)。
空间复杂度为 O(1) 的迭代代码:
如果仔细观察上述代码,就能发现计算 dp[i] 时只需要用到 dp[i - 1] 和 dp[i - 2] 这两个值,也就是说,只需要缓存两个值就足够了,并不需要一个长度为 n 的数组,因此,可以进一步优化代码的空间效率。
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
int a = nums[0];
if (n == 1)
return a;
int b = max(nums[0], nums[1]);
for (int i = 2; i < n; ++i)
{
int c = max(a + nums[i], b);
a = b;
b = c;
}
return b;
}
};
优化之后的代码的空间复杂度是 O(1),但时间复杂度仍然是 O(n)。
面试题 90 : 环形房屋偷盗
题目:
一条环形街道上有若干房屋。输入一个数组表示该条街道上的房屋内财产的数量。如果这条街道上相邻的两幢房屋被盗就会自动触发报警系统。请计算小偷在这条街道上最多能偷取的财产的数量。例如,街道上 5 家的财产用数组 [2, 3, 4, 5, 3] 表示,如果小偷到下标为 1 和 3 的房屋内盗窃,那么他能偷取到价值为 8 的财产,这是他在不触发报警系统的情况下能偷取到的最多的财物,如下图所示。被盗房屋上方用特殊符号标出。
分析:
这个问题和面试题 89 类似,唯一的区别在于面试题 89 中的房屋排成一排,而这个题目中的房屋围成一个环。线形街道上的房屋和环形街道上的房屋存在不同之处。如果 n 幢房屋围成一个首尾相接的环形,那么标号为 0 的房屋和标号为 n - 1 的房屋相邻,如果小偷进入这两幢房屋内都偷东西就会触发报警系统。例如,5 幢房屋内的财产数量分别是 2、3、4、5、3。如果这 5 幢房屋排成一排,小偷如果到标号为 0、2、4 的这 3 幢房屋内偷东西,那么他能偷得价值为 9 的财物。如果这 5 幢房屋围成一个首尾相接的环,由于标号为 0 和 4 的房屋相邻,因此小偷不能同时进入这两幢房屋内偷东西。
由于这个问题和面试题 89 的区别在于小偷不能同时到标号为 0 和 n - 1 的两幢房屋内偷东西。如果他考虑去标号为 0 的房屋,那么他一定不能去标号为 n - 1 的房屋;如果他考虑去标号为 n - 1 的房屋,那么他一定不能去标号为 0 的房屋。因此,可以将这个问题分解成两个子问题:一个问题是求小偷从标号为 0 开始到标号为 n - 2 结束的房屋内能偷得的最多财物数量,另一个问题是求小偷从标号为 1 开始到标号为 n - 1 结束的房屋内能偷得的最多财物数量。小偷从标号为 0 开始到标号为 n - 1 结束的房屋内能偷得的最多财物数量是这两个子问题的解的最大值。
可以将面试题 89 的代码稍做修改,这样就可以定义出一个函数使其求出小偷从标号 start 开始到 end 结束的范围内能偷得的最多财物数量,接着分别输入标号从 0 到 n - 2 和从 1 到 n - 1 这两个范围调用该函数就可以解决这个问题。
代码实现:
cpp
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 1)
return nums[0];
int result1 = helper(nums, 0, n - 2);
int result2 = helper(nums, 1, n - 1);
return max(result1, result2);
}
private:
int helper(vector<int>& nums, int start, int end) {
int a = nums[start];
if (start == end)
return a;
int b = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; ++i)
{
int c = max(a + nums[i], b);
a = b;
b = c;
}
return b;
}
};