动态规划–Day04–打家劫舍–740. 删除并获得点数,3186. 施咒的最大总伤害,2140. 解决智力问题
今天要训练的题目类型是:【打家劫舍】,题单来自@灵艾山茶府。
掌握动态规划(DP)是没有捷径的,咱们唯一能做的,就是投入时间猛猛刷题。
动态规划要至少刷100道才算入门!
记忆化搜索是新手村神器。方便理解,写完之后可以转译成递推。
但是有些题目只能写递推,才能优化时间复杂度。熟练之后直接写递推也可以。
740. 删除并获得点数
思路【我】:
终于能自己做出来一题打家劫舍了!!!
- 首先,使用map记录每种元素与对应的总和:map<元素,元素总和>,比如nums里面是[3,5,5]那么,map[5] = 10;
- 考虑选i,那么i-1不能选,但是怎么把i+1的数删去?
- 那就直接站在i-1的角度思考问题。
- 情况一:如果i-1要选,那么i-2不能选,i不能选
- 情况二:要么i-1不选,那么i-2可以选,i可以选
- 取两者的较大值
class Solution {
public int deleteAndEarn(int[] nums) {
int n = nums.length;
int[] map = new int[10004];
// map<元素,元素总和>,比如nums里面是[3,5,5]那么,map[5] = 10;
for (int i = 0; i < n; i++) {
map[nums[i]] += nums[i];
}
int[] f = new int[10004];
f[1] = map[1];
for (int i = 2; i < map.length; i++) {
// 选i,那么i-1不能选,但是怎么把i+1的数删去?
// 那就直接站在i-1的角度思考问题。
// 考虑i-1,如果i-1要选,那么i-2不能选,i不能选
// 要么i-1不选,那么i-2可以选,i可以选
int res1 = f[i - 1];
int res2 = f[i - 2] + map[i];
f[i] = Math.max(res1, res2);
}
return f[f.length - 1];
}
}
上面是一刷时候的题解。发现计算f[i]的时候并不会影响map[i],那么可以直接在map[i]上面计算,节省空间。
class Solution {
public int deleteAndEarn(int[] nums) {
int n = nums.length;
int[] map = new int[10004];
for (int i = 0; i < n; i++) {
map[nums[i]] += nums[i];
}
for (int i = 2; i < map.length; i++) {
int res1 = map[i - 1];
int res2 = map[i - 2] + map[i];
map[i] = Math.max(res1, res2);
}
return map[map.length - 1];
}
}
思路:打家劫舍
后来仔细看了一下,情况一是选i-1,情况二是选i和i-2,这不就是跟198. 打家劫舍一模一样嘛。
区别就是,打家劫舍,是已经给出每家的值了,而本题要自己构建map,才是要打家劫舍的nums。
class Solution {
public int deleteAndEarn(int[] nums) {
int n = nums.length;
int[] map = new int[10004];
// map<元素,元素总和>,比如nums里面是[3,5,5]那么,map[5] = 10;
for (int i = 0; i < n; i++) {
map[nums[i]] += nums[i];
}
return rob(map);
}
// 198. 打家劫舍(空间优化)
private 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;
}
}
3186. 施咒的最大总伤害
思路:
因为power[i]的取值范围是1e9,所以不能直接开一个map[1e9]。只能先用HashMap记录<元素,元素总和>,再转为int[][] map
处理:map[][0]:元素,map[][1]:元素总和
。
map[][0]:元素,map[][1]:元素总和
,·按照map[][0],即元素值排序
。- 初始化,讨论每个f[i],但是f[i]的值放到f[i+1]
while (map[j][0] < map[i][0] - 2) j++;
,这样,出循环之后,就是满足map[j][0] >= map[i][0] -2
。如果选i的话,j与它拉开了安全距离- 情况一:i不选,
f[i] = f[i-1]
- 情况二:i选,
f[i] = f[j-1] + map[i][1];
- 因为f[i]的值放到f[i+1],所以f的索引整体+1,map索引不变。最终的递推公式为:
f[i + 1] = Math.max(f[i], f[j] + map[i][1]);
- f[i+1] 是「处理完 a[i] 后的最大伤害」—— 它是状态的索引,不是「元素的索引」。比如:
- f[0]:处理 0 个元素(无元素),最大伤害 0;
- f[1]:处理完 a[0] 后的最大伤害;
- f[2]:处理完 a[1] 后的最大伤害;
- f[n]:处理完 a[n-1] 后的最大伤害(最终答案)。
class Solution {
public long maximumTotalDamage(int[] power) {
// hmap<元素,元素总和>
Map<Long, Long> hmap = new HashMap<>();
for (long x : power) {
hmap.merge(x, x, Long::sum);
}
int n = hmap.size();
// map[][0]:元素,map[][1]:元素总和
long[][] map = new long[n][2];
int p = 0;
for (Map.Entry<Long, Long> e : hmap.entrySet()) {
map[p][0] = e.getKey();
map[p][1] = e.getValue();
p++;
}
// 按照map[][0],即元素值排序
Arrays.sort(map, (a, b) -> Long.compare(a[0], b[0]));
// 初始化,讨论每个f[i],但是f[i]的值放到f[i+1]
long[] f = new long[n + 1];
int j = 0;
for (int i = 0; i < n; i++) {
// 出循环之后,就是满足map[j][0] >= map[i][0] -2
while (map[j][0] < map[i][0] - 2) {
j++;
}
// 情况一:i不选,f[i] = f[i-1]
// 情况二:i选,f[i] = f[j-1] + map[i][1];
// 因为f[i]的值放到f[i+1],所以f的索引整体+1,map索引不变
f[i + 1] = Math.max(f[i], f[j] + map[i][1]);
}
return f[n];
}
}
// 而 f[i+1] 是「处理完 a[i] 后的最大伤害」—— 它是状态的索引,不是「元素的索引」。比如:
// f[0]:处理 0 个元素(无元素),最大伤害 0;
// f[1]:处理完 a[0] 后的最大伤害;
// f[2]:处理完 a[1] 后的最大伤害;
// …
// f[n]:处理完 a[n-1] 后的最大伤害(最终答案)。
真的是给一颗糖打一棒子。上道题刚做出来开心,这道题又绕晕了。
2140. 解决智力问题
方法:记忆化搜索
思路:
好耶,又自己做出来一道打家劫舍。
这道题和普通打家劫舍的区别:
- 要从前往后搜索
- 讨论i,如果要选i的话,要计算出下一个去到的索引值next
具体步骤:
- 初始化记忆数组:
Arrays.fill(memo, -1);
- 从索引0开始从前往后搜索
- 如果索引越界,返回0
- 有记忆(缓存),返回缓存
if (memo[i] != -1) return memo[i];
- 没有记忆,这个状态没有探索过
- 情况一,不选i。(看下一个索引)dfs(i+1)
- 情况二:选i(i能获得的分数 + 去下一个能去的索引next探索)
- 选两种情况的较大值。写入记忆,返回。
class Solution {
public long mostPoints(int[][] questions) {
// 记忆化搜索。记忆数组初始值为-1,表示没有记忆(没有缓存)
long[] memo = new long[questions.length];
Arrays.fill(memo, -1);
// 从0开始往后搜索
return dfs(0, questions, memo);
}
private long dfs(int i, int[][] ques, long[] memo) {
// 如果索引越界,返回0
if (i >= ques.length) {
return 0;
}
// 有记忆(缓存),返回缓存
if (memo[i] != -1) {
return memo[i];
} else { // 没有记忆,这个状态没有探索过
// 情况一,不选i。(看下一个索引)
long res1 = dfs(i + 1, ques, memo);
// 情况二:选i(i能获得的分数 + 去下一个能去的索引next探索)
int next = i + ques[i][1] + 1;
long res2 = ques[i][0] + dfs(next, ques, memo);
// 选两种情况的较大值。写入记忆,返回。
memo[i] = Math.max(res1, res2);
}
return memo[i];
}
}
方法:动态规划(递推)
思路:
这时候f[]要申请n+1的长度,因为要用到f+1;
递归如果是从前往后的话,那么递推就要从后往前遍历了。
这时候要注意next的取值范围。
其余内容同上。所以@灵艾山茶府说,是“1:1翻译成递推”
class Solution {
public long mostPoints(int[][] questions) {
int n = questions.length;
long[] f = new long[n + 1];
for (int i = n - 1; i >= 0; i--) {
// 情况一,不选i。(看下一个索引)
long res1 = f[i + 1];
// 情况二:选i(i能获得的分数 + 去下一个能去的索引next探索)
int next = i + questions[i][1] + 1;
// next索引不能越界
next = next >= n ? n : next;
long res2 = questions[i][0] + f[next];
// 选两种情况的较大值。
f[i] = Math.max(res1, res2);
}
return f[0];
}
}