LeetCode 740. 删除并获得点数
题目描述


题意分析
每次选择并删除一个值
x,可以得到x点分数,同时必须删除数组中所有值为x - 1和x + 1的元素。被连带删除的元素不提供分数,求最终能得到的最大总分。冲突由数值相差一决定,与元素在原数组中的下标是否相邻无关。同值元素不会互相删除,可以继续分别选择;输入顺序不影响最优结果。
解法:转化为打家劫舍
核心思路
[!blue]
一旦决定选择值
x,相邻数值的元素就不能再贡献分数,但其他等于x的元素仍然存在。把这些同值元素全部选完只会增加收益,不会产生新的冲突,所以可以预先合并:sum[x]表示所有值为x的元素的分数总和,即数值乘以出现次数。聚合后,只需在连续数轴上选择若干个值,不能同时选择相邻的两个数值,权重是
sum[x],这就变成了打家劫舍。收益数组包含从零到最大值的所有位置,没有出现的值记为零收益;保留这些空档,才能正确表达距离大于一的值之间没有冲突。处理当前数值
i之前,take表示考虑到i - 1且选择i - 1的最优收益,skip表示考虑到i - 1且不选择它的最优收益。选择i必须跳过i - 1,因此takeNew = skip + sum[i];不选择i就可以接受前一位置的任意状态,因此skipNew = max(take, skip)。两个新状态都必须从同一轮旧状态计算完,再一起覆盖。选或不选覆盖了当前值的全部合法选择,逐步保留两类最优结果,就能得到数轴前缀的整体最优解。最后最大数值也不一定值得选,答案应取最终
take、skip的较大值。
解题步骤
- 找到数组中的最大值
M,建立长度为M + 1的收益数组。- 对每个原元素
x,执行sum[x] += x,聚合同值收益。- 初始化
take = skip = 0,依次处理数值0到M,包括零收益的空档。- 先计算选择当前值和跳过当前值的两个新状态,再覆盖旧状态。
- 返回最终两种状态的最大值。
代码实现
class Solution {
// 若选择 x,则不能选择 x-1 和 x+1,等价于打家劫舍问题。
public int deleteAndEarn(int[] nums) {
int maxVal = 0;
for (int num : nums) {
maxVal = Math.max(maxVal, num);
}
int[] sum = new int[maxVal + 1];
for (int num : nums) {
// 同值全部汇总收益,冲突按相邻数值而非原下标判断
sum[num] += num;
}
int take = 0;
int skip = 0;
for (int i = 0; i <= maxVal; i++) {
// 新状态都读取上一轮,不能提前覆盖旧的选与不选状态
int takeNew = skip + sum[i];
int skipNew = Math.max(skip, take);
take = takeNew;
skip = skipNew;
}
return Math.max(take, skip);
}
}
func deleteAndEarn(nums []int) int {
// 若选择 x,则不能选择 x-1 和 x+1,等价于打家劫舍问题。
maxVal := 0
for _, v := range nums {
if v > maxVal {
maxVal = v
}
}
sum := make([]int, maxVal+1)
for _, v := range nums {
// 同值全部汇总收益,冲突按相邻数值而非原下标判断
sum[v] += v
}
take, skip := 0, 0
for i := 0; i <= maxVal; i++ {
// 新状态都读取上一轮,不能提前覆盖旧的选与不选状态
takeNew := skip + sum[i]
skipNew := skip
if take > skipNew {
skipNew = take
}
take, skip = takeNew, skipNew
}
if take > skip {
return take
}
return skip
}
复杂度分析
- 时间复杂度:$O(n + M)$,
n为原数组长度,先聚合元素,再扫描完整数值范围。- 空间复杂度:$O(M + 1)$,用于收益表;动态规划只保存两个滚动状态。
关键点总结
[!green]
- 同值元素可以全部获得收益,聚合后只需处理数值间的相邻冲突。
- 数值空档必须保留为零收益位置,不能随意把所有出现值压成相邻位置。
- 当前选择只能连接前一位置的不选状态,当前不选则接前一轮两类最优值。
- 两个新值都算完再滚动,避免同一轮状态互相污染。
易错点总结
[!yellow]
- 根据原数组下标判断冲突,误把位置相邻当成了数值相邻。
- 聚合时只统计次数,没有乘上数值,改变了每次操作获得的分数。
- 只按出现过的值顺次做打家劫舍,却不检查数值空档,会把不相差一的值误判为冲突。
- 先覆盖
take再计算skip,会将本轮选择混入前一轮状态。- 最终只返回
take,相当于强制选择最大数值,可能错过更好的跳过方案。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 198. 打家劫舍 | 中等 | 先按数值聚合全部收益,再把相邻数值不能同时选择转成线性打家劫舍。 |
| 213. 打家劫舍 II | 中等 | 比较选择当前元素与跳过当前元素的最优值;本题按值累计收益后禁止选择相邻值,该题拆开环形首尾冲突的两种情况。 |
| 337. 打家劫舍 III | 中等 | 比较选择当前元素与跳过当前元素的最优值;本题按值累计收益后禁止选择相邻值,该题树上分别返回选根与不选根的结果。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!