动态规划–Day03–打家劫舍–198. 打家劫舍,213. 打家劫舍 II,2320. 统计放置房子的方式数
今天要训练的题目类型是:【打家劫舍】,题单来自@灵艾山茶府。
掌握动态规划(DP)是没有捷径的,咱们唯一能做的,就是投入时间猛猛刷题。
动态规划要至少刷100道才算入门!
记忆化搜索是新手村神器。方便理解,写完之后可以转译成递推。
但是有些题目只能写递推,才能优化时间复杂度。熟练之后直接写递推也可以。
198. 打家劫舍
方法:记忆化搜索(递归)
思路:
和爬楼梯几乎是同一个模板的。爬楼梯是memo[i] = res1 + res2;
,而打家劫舍是memo[i] = Math.max(res1, res2);
后来忽然发现了一个细微的差别,爬楼梯,缓存一般是开n+1位,而打家劫舍缓存一般开n位就够了。
- 使用memo记忆,缓存已经探索过的节点
- 从数组最后一个值开始往前探索
- 递归结束判断:如果索引超出范围,则返回
- 如果memo有值,用缓存值
- 对于当前探索的i这个节点,能不能偷,有两个情况:
- 情况一:偷i-1家,那么i家就不能偷(递归进去探索i-1节点)
- 情况二:偷i-2家,那么i家也能偷(递归进去探索i-2节点)
- 返回两种情况的较大值,顺便保存到记忆
class Solution {
public int rob(int[] nums) {
int n = nums.length;
// 记忆(缓存),默认未使用标志为-1
int[] memo = new int[n];
Arrays.fill(memo, -1);
// 从索引n-1开始搜索(也就是数组的最后一个值)
return dfs(n - 1, nums, memo);
}
private int dfs(int i, int[] nums, int[] memo) {
// 索引超出范围,返回
if (i < 0) {
return 0;
}
// 如果已经判断过这种情况了,直接用记忆(缓存)
if (memo[i] != -1) {
return memo[i];
} else {
// 情况一:偷i-1家,那么i家就不能偷
// 情况二:偷i-2家,那么i家也能偷
int res1 = dfs(i - 1, nums, memo);
int res2 = dfs(i - 2, nums, memo) + nums[i];
// 返回两种情况的较大值,顺便保存到记忆
memo[i] = Math.max(res1, res2);
}
return memo[i];
}
}
从dfs函数来看,一进去先判断索引是否超出范围,超出了则返回。其实是浪费多了一次资源(因为调用函数要压栈,出栈等,时间空间都变慢了)
可以改成这样:先判断索引,再考虑调用函数:
private int dfs(int i, int[] nums, int[] memo) {
if (memo[i] != -1) {
return memo[i];
} else {
// 先判断索引合法,再调用函数
int res1 = 0;
if (i - 1 >= 0) {
res1 = dfs(i - 1, nums, memo);
}
// res2要赋值为nums[i]
int res2 = nums[i];
if (i - 2 >= 0) {
res2 = dfs(i - 2, nums, memo) + nums[i];
}
memo[i] = Math.max(res1, res2);
}
return memo[i];
}
注意,这里res2要默认赋值为nums[i]。因为这是一个决策,是决定偷i-2,那么i家先偷了,再去i-2,然后发现i-2没有人住(超出索引范围),那么i家我也偷到了。
如果写成这样是错的:
private int dfs(int i, int[] nums, int[] memo) {
if (memo[i] != -1) {
return memo[i];
} else {
int res1 = 0;
int res2 = 0;
if (i - 1 >= 0) {
res1 = dfs(i - 1, nums, memo);
}
if (i - 2 >= 0) {
res2 = dfs(i - 2, nums, memo) + nums[i];
}
memo[i] = Math.max(res1, res2);
}
return memo[i];
}
方法:动态规划(递推)
思路:
递推要初始化,因为f[i]需要依赖f[i-1]和f[i-2],所以要初始化前两个数,还要特判n==1的情况。
class Solution {
public int rob(int[] nums) {
int n = nums.length;
// 如果只有一家,直接偷,返回。
if (n == 1) {
return nums[0];
}
int[] f = new int[n];
// 递推要初始化,因为f[i]需要依赖f[i-1]和f[i-2],所以要初始化前两个数
f[0] = nums[0];
f[1] = Math.max(nums[0], nums[1]);
// 从第三个数开始遍历
for (int i = 2; i < n; i++) {
// 情况一:偷i-1家,那么i家就不能偷
// 情况二:偷i-2家,那么i家也能偷
int res1 = f[i - 1];
int res2 = f[i - 2] + nums[i];
// 偷较大值
f[i] = Math.max(res1, res2);
}
// 最后的结果,在最后一家判断完之后,包含了所有的情况。
// 所以答案在最后一个索引的位置。
return f[n - 1];
}
}
思路:
把f数组索引直接+2。上一篇代码的f[0]的值,放到这一题代码的f[2]的位置。
不管f[0]和f[1],直接不用他们。
class Solution {
public int rob(int[] nums) {
int n = nums.length;
int[] f = new int[n + 2];
for (int i = 0; i < n; i++) {
f[i + 2] = Math.max(f[i + 1], f[i] + nums[i]);
}
return f[n + 1];
}
}
思路:
空间优化的写法。
class Solution {
public int rob(int[] nums) {
int f0 = 0;
int f1 = 0;
for (int x : nums) {
int newF = Math.max(f1, f0 + x);
f0 = f1;
f1 = newF;
}
return f1;
}
}
这些都是写完之后的优化版本,如果第一次写,不要想复杂的,先按照自己内心第一思路,把题目写完先,再想优化。
213. 打家劫舍 II
方法:动态规划(递推)
思路:
- 特判索引0位置,把剩余位置变为非循环数组进行操作。
- 索引0位置只有两种选择,偷或者不偷。
- 偷索引0,那么索引1和索引n-1不能偷。问题变为从索引2到n-2的非循环数组。
- 不偷索引0。问题变为从索引1到n-1的非循环数组。
- 取二者较大值
class Solution {
public int rob(int[] nums) {
int n = nums.length;
// 偷索引0,那么索引1和索引n-1不能偷。问题变为从索引2到n-2的非循环数组。
int res1 = nums[0] + rob1(nums, 2, n - 2);
// 不偷索引0。问题变为从索引1到n-1的非循环数组。
int res2 = rob1(nums, 1, n - 1);
// 取二者较大值
return Math.max(res1, res2);
}
// 上一题的空间优化版本
private int rob1(int[] nums, int start, int end) {
int f0 = 0;
int f1 = 0;
// [start,end] 左闭右闭
for (int i = start; i <= end; i++) {
int newF = Math.max(f1, f0 + nums[i]);
f0 = f1;
f1 = newF;
}
return f1;
}
}
2320. 统计放置房子的方式数
方法:动态规划(递推)
思路:
- 两边互相独立,求一边,最后f[n] * f[n]即可。
- 初始化f[0]和f[1]。如果没有空地,只有不放这1种选择;如果只有一块空地,有放和不放2种选择。
- 对于每个f[i]:
- 情况一:如果不放f[i]的话,f[i-1]可放可不放
- 情况二:如果放f[i]的话, f[i-2]可放可不放
- 两种情况加起来,就是f[i]可放可不放
- 最后返回结果时:
- 注意,两个MOD相加,不会溢出int。但是两个MOD相乘,会溢出int
- 所以要先转为long,相乘,取模,后再转为int
class Solution {
public int countHousePlacements(int n) {
// 两边互相独立,求一边,最后f[n] * f[n]即可
final int MOD = 1000000007;
int[] f = new int[n + 2];
// 初始化。如果没有空地,只有不放这1种选择;如果只有一块空地,有放和不放2种选择。
f[0] = 1;
f[1] = 2;
for (int i = 2; i <= n; i++) {
// 情况一:如果不放f[i],f[i-1]可放可不放
// 情况二:如果放f[i], f[i-2]可放可不放
// 两种情况加起来,就是f[i]可放可不放
f[i] = (f[i - 1] + f[i - 2]) % MOD;
}
// 注意,两个MOD相加,不会溢出int。但是两个MOD相乘,会溢出int
// 所以要先转为long,相乘,取模,后再转为int
return (int) ((long) f[n] * f[n] % MOD);
}
}
总感觉这道题跟《打家劫舍》没什么关系,反而像是《爬楼梯》。每个位置都可爬可不爬,然后结果加起来。
f[i] = (f[i - 1] + f[i - 2])
。初步是这么想,多做题,回头再看看。
关于取模运算,可以看@灵艾山茶府的这篇文章:模运算的世界:当加减乘除遇上取模。