除了爬楼梯类问题外,入门DP的另一大类即是打家劫舍问题。
题单⬆️(0x3F总结版,特别鸣谢)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋,可以偷窃到最高总金额。如果只有两间房屋,则由于两间房屋相邻,不能同时偷窃,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃,可以偷窃到最高总金额。
如果房屋数量大于两间,应该如何计算能够偷窃到的最高总金额呢?对于第 k (k>2) 间房屋,有两个选项:
偷窃第 k 间房屋,那么就不能偷窃第 k−1 间房屋,偷窃总金额为前 k−2 间房屋的最高总金额与第 k 间房屋的金额之和。
不偷窃第 k 间房屋,偷窃总金额为前 k−1 间房屋的最高总金额。
在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
用 dp[i] 表示前 i 间房屋能偷窃到的最高总金额,那么就有如下的状态转移方程:
dp[i]=max(dp[i−2]+nums[i],dp[i−1])
边界条件为:
dp[0]=nums[0]
dp[1]=max(nums[0],nums[1])
只有一间房屋,则偷窃该房屋
只有两间房屋,选择其中金额较高的房屋进行偷窃
最终的答案即为 dp[n−1],其中 n 是数组的长度。
class Solution {
public:
int rob(vector& nums) {
int n = nums.size();
if(nums.empty()) {
return 0;
}
if (n == 1) {
return nums[0];
}
vectordp (n, 0);
dp[0] = nums[0];
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];
}
};
好,你现在已经学会最基本的打家劫舍了,那我们试着加一点难度。
这道题与打家劫舍极为相似,只需遍历nums,用一个数组points[i] 表示每个i * 次数
class Solution {
public:
int deleteAndEarn(vector& nums) {
if (nums.empty()) {
return 0;
}
int maxVal = *max_element(nums.begin(), nums.end());
vector points(maxVal + 1, 0);
vector dp(maxVal + 1, 0);
for (int num : nums) {
points[num] += num;
}
dp[1] = points[1];
for (int i = 2; i <= maxVal; i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + points[i]);
}
return dp[maxVal];
}
};
这个题可以把每一侧道路都看作一个“不能放邻近房子” 问题,因此,单侧就变成了一个经典的斐波那契型递归问题。
而两侧结果是独立的 互不影响,因此最后结果是单侧的平方
class Solution {
public:
int countHousePlacements(int n) {
const int MOD = 1e9 + 7;
vector dp(n + 1, 0);
dp[0] = 1;
dp[1] = 2;
for (int i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % MOD;
}
long long res = (dp[n] * dp[n]) % MOD;
return res;
}
};
补充非常重要的一点 : 在代码中,我们取模了两次,因此有同学可能会疑惑,取模两次会不会造成错误结果。
其实,这是一个非常常见的问题,在for循环中的,dp[i] 取模式为了防止中间爆 long long 因此必须取余一次。
所以,我们通常的做法是:
• 所有加法中都及时 % MOD,防止中间结果超限;
• 最后再对乘法结果 % MOD,是对的;
• 即使前面的 dp[n] 已经 % MOD,我们也要对最终乘积 res 再 % MOD。
为什么?
举个例子说明:
MOD = 1000000007;
dp[n] = 1000000006; // 即使已经 % MOD
res = (dp[n] * dp[n]) % MOD;
// 即 (1000000006 * 1000000006) % MOD = 1
如果不在res中 % MOD ,可能res会溢出。
综上所述,对dp取余,又对res取余不是重复,是必须的!
本题与打家劫舍 I 主要区别在于房子形成环形,因此,我们可以把环拆成两种情况处理,
1. 不偷第一个房子 考虑区间【1,n - 1】
2. 不偷最后一个房子 考虑区间【0,n - 2】
因此,我们可以讲打家劫舍 I 的函数封装为一个函数,并分别带入两种情况,求出最大值。
代码如下 :
class Solution {
public:
int robb(vector& nums, int start, int end) {
int len = end - start + 1;
if (len == 1) {
return nums[start];
}
vector dp(len, 0);
dp[0] = nums[start];
dp[1] = max(nums[start], nums[start + 1]);
for (int i = 2; i < len; i++) {
dp[i] = max(dp[i - 1], dp[i - 2] + nums[start + i]);
}
return dp[len - 1];
}
int rob(vector& nums) {
int n = nums.size();
if (n == 0) return 0;
if (n == 1) return nums[0];
//不偷第一个
int case1 = robb(nums, 1, n - 1);
//不偷最后一个
int case2 = robb(nums, 0, n - 2);
return max(case1, case2);
}
};
代码中有一下需注意⚠️:
1. 在robb 函数中,需加入 if 判断以避免求 dp[1] 时报错
2. robb函数中nums是从 start 开始的,不要傻乎乎写 nums[0] nums[i] 哦
/// dp[i] = max(dp[x]..., dp[y] + power[i] * count[power[i]]) :
/// power[i] > power[x] >= power[i] - 2}
/// y(y& power) {
/// 记录每个‘伤害’的个数
unordered_map countMap;
for (int x : power) countMap[x]++;
/// 排序
sort(power.begin(), power.end());
/// 去重
auto new_end = unique(power.begin(), power.end());
power.erase(new_end, power.end());
//DP
int size = power.size();
vector dp(size);
dp[0] = countMap[power[0]] * power[0];
long long result = dp[0];
for (int i = 0; i < size; i++) {
long long curVal = power[i] * countMap[power[i]];
dp[i] = curVal;
for (int j = 1; j <= 3; j++) {
if (i - j < 0) continue;
if (power[i] - 2 > power[i - j]) {
dp[i] = max(dp[i], dp[i - j] + curVal);
break;
} else {
dp[i] = max(dp[i], dp[i - j]);
}
}
result = max(result, dp[i]);
}
return result;
}
};