删除并获得点数 图解题解
这道题到底在问什么
- nums
- [2,2,3,3,3,4,6,6]
- 输出
- 21
- 解释
- 选 3 三次(9)+选 6 两次(12)=21,2 和 4 被连带删光
先想最直接的笨办法
先一遍统计 total,再一遍线性填 dp,每格只看前两格,整体线性,比指数枚举快到天上去。(动画第 26 步)
最优解:为什么这么做
一句话答案:LeetCode 740 删除并获得点数:选一个值 x 就得删光所有 x-1 和 x+1,冲突只在数值相邻。先把每个值的总收益折进 sum 数组,就化归成打家劫舍——相邻的值不能同选。线性填 dp,时间 O(n+M)、空间 O(1)。
删除并获得点数到底在求什么
给一个数组 nums,每次挑一个值 x:删掉所有等于 x 的数、得分 x,代价是连带删光所有 x-1 和 x+1(不得分)。反复删空求最高总分。nums = [2,2,3,3,3,4,6,6] 的答案是 21:选值 3 三次拿 9 分、选值 6 两次拿 12 分,值 2、4 被连带清掉。
为什么每次选最大的会算错
最顺手的贪心(每步只挑眼前最好的、不顾以后)是每次选最大的值。可选大数会连累两边小数,选了 4 就废掉 3 那三个数。穷举也不行:每个值选或不选是两个分支,值域(所有可能出现的数值,本例 0 到 6)一大,方案数就指数级炸开,而后半段大量重复被反复重算。
为什么这题能变成打家劫舍
值 v 的总收益是 v 乘它的出现次数,扫一遍折进 sum 数组。本例 sum[2]=4、sum[3]=9、sum[4]=4、sum[6]=12,其余为 0。
把值域 0 到 6 排成一排,每格放 sum[v]。约束「选了 v 就不能选 v-1 和 v+1」,就是这排里选一个就不能选左右紧挨的邻居,跟打家劫舍一模一样:一排房子不能偷相邻两家。冲突只在数值相邻,跟它们在原数组排第几无关,按值排一排才对得上。
选了 v 为什么只能接 dp[v-2]
定义 dp[v] 为「只考虑值 0 到 v、不选相邻值时能拿的最大分」。到值 v 两种互斥选择:不选就继承 dp[v-1];选 v 拿 sum[v],但选了 v 排斥 v-1,不能接 dp[v-1],只能接隔开一格的 dp[v-2]。取大,dp[v] = max(dp[v-1], dp[v-2] + sum[v])。
最容易写错的是选 v 时接成相邻的 dp[v-1],等于让 v 和 v-1 同时被选。接 dp[v-2] 才跳过相邻那格。dp[v] 只依赖前两格,两个滚动变量就够。
拿题目样例把 dp 填一遍
sum = [0, 0, 4, 9, 4, 0, 12],下标就是值。dp[0]=0、dp[1]=0。dp[2] 选 sum[2]=4 接 dp[0]=0 得 4。dp[3] 选 sum[3]=9 接 dp[1]=0 得 9。dp[4] 选 sum[4]=4 接 dp[2]=4 只有 8,不如不选继承的 9,取 9。dp[5] 值 5 没出现,选 0 接 dp[3]=9 得 9。dp[6] 选 sum[6]=12 接 dp[4]=9 得 21。最后 dp[6]=21 就是答案,对应值 3 加值 6,和题目吻合。
复杂度多少,sum 和防溢出两处别踩
统计 sum 扫原数组 O(n),填 dp 扫值域 O(M),M 为最大值,合起来 O(n+M)。sum 数组按值域开 O(M),dp 两个变量滚动 O(1)。
两个坑:一是 sum[v] 存 v 乘次数的总得分,写成「出现次数」就把 9 分记成 3 个;二是次数多、值大时总分会超 int,C++ 和 Java 得用 long。还有个隐蔽点:值 5 这种没出现的空格也要填 dp,隔开值 4 和值 6,dp[6] 才接得到含选 3 的 dp[4]。
▶ 动画逐步走查(共 26 步)——想跟着动画一帧帧对照就展开
- 3贪心地「每次选当前最大的」会出错:选大数可能丢掉两边一堆小数。这种「选了就排斥邻居」的味道,得换个角度看。
- 4选值 v,能拿的分 = v × 它出现的次数。我们先扫一遍原数组,把「有几个」一次性折叠进 total[v]。下面看着它一格一格累出来。
- 5指针停在起点下面这排是原数组。指针从最左往右走,每碰到一个数 v,就把它的得分 v 加到右边 total[v] 上。盯住右边累计值怎么涨。
- 6total[2] += 2第一个数是 2,把 2 加进 total[2],它从 0 变 2。指针继续右移。
- 7total[2] += 2又是一个 2,total[2] 再加 2 变成 4——这就是「两个 2 一起拿 = 4 分」被折叠进了一格。
- 8total[3] += 3轮到第一个 3,给 total[3] 加 3。值 2 那两格已经扫完(变灰)。
- 9total[3] += 3第二个 3,total[3] 涨到 6。三个 3 还差最后一个。
- 10total[3] += 3第三个 3 加完,total[3] 到 9——选值 3 一次性能拿 9 分。
- 11total[4] += 4数到 4,给 total[4] 加 4。它紧挨着 3,选 3 就会把它清掉。
- 12total[6] += 6第一个 6 入账,total[6] 变 6。注意 6 和前面的 4 之间隔着个 5,互不相邻。
- 13total[6] += 6最后一个 6 加完,total[6] 到 12——两个 6 折成一格 12 分。整个数组扫完,统计阶段结束。
- 14把值域 0~6 排成一排,每格放它的 total[v]。选一个格子就不能选它左右紧挨着的格子——这完全等价于打家劫舍(不能偷相邻两家)!
- 15下标=值,柱高=total[v]把刚统计的 total 摆成柱子:下标 0~6 就是值,柱高就是 total[v]。值 5 没出现(柱高 0),它把 4 和 6 隔开,于是选 3 又选 6 互不冲突。开始填表。
- 16dp[v] 表示「只考虑值 0..v 时能拿的最大分」。要么不选 v(继承 dp[v-1]),要么选 v(拿 total[v],但只能接 dp[v-2])。两者取大。
- 17dp[v] = 考虑值 0..v 的最大分上一行是 total,下一行是要填的 dp。dp[v] 的含义:只在值 0..v 里挑、且不挑相邻值,能拿的最大分。
- 18dp[0] = total[0] = 0只有值 0 可选时,最大分就是它自己的 total[0]=0(本例没有 0)。这是不依赖别人的地基,第一格填好。
- 19dp[1] = max(dp[0], total[1])考虑值 0、1:要么不选 1(拿 dp[0]=0),要么选 1(拿 total[1]=0)。两者都是 0,dp[1]=0。第二格填好。
- 20dp[2] = max(dp[1], dp[0]+total[2])到值 2:不选继承相邻的 dp[1]=0;选就拿 total[2]=4,但不能接相邻 dp[1],只能接隔开的 dp[0]=0 → 得 4。取大,dp[2]=4。
- 21dp[3] = max(dp[2], dp[1]+total[3])到值 3:不选继承 dp[2]=4;选拿 total[3]=9,跳过相邻 dp[2],接 dp[1]=0 → 得 9。9 > 4,dp[3]=9。选 3 比保住 2 更划算。
- 22dp[4] = max(dp[3], dp[2]+total[4])到值 4:不选继承 dp[3]=9;选拿 total[4]=4,接 dp[2]=4 → 共 8。9 > 8,dp[4]=9——选 4 反而因放弃 3 而变少,所以保住 3。
- 23dp[5] = max(dp[4], dp[3]+total[5])值 5 没出现,total[5]=0。不选继承 dp[4]=9;选它拿 0、接 dp[3]=9,也是 9。dp[5]=9 保持不变——但这一格很重要,它把 4 和 6 隔开,让 6 能接到含 3 的状态。
- 24dp[6] = max(dp[5], dp[4]+total[6])到值 6:不选继承 dp[5]=9;选拿 total[6]=12,跳过相邻 dp[5],接 dp[4]=9(里头含了选 3 的 9 分)→ 共 21。21 > 9,dp[6]=21。这就是「选 3 又选 6」被表达了出来。
- 25答案 = dp[6] = 21dp 这一行滚到最后一格 dp[6]=21 就是答案。对应方案:选值 3(9 分)+ 选值 6(12 分),它们隔得远不冲突。和示例答案 21 对上了。
- 26先一遍统计 total,再一遍线性填 dp,每格只看前两格,整体线性,比指数枚举快到天上去。
- 29约束是「选了 v 不能选 v±1」,冲突发生在数值相邻上,跟它们在原数组的位置无关。所以把值排成一排来 DP,先排序去重也行。
- 31选 3 时误接 dp[2]=4如果选值 3 时去接相邻的 dp[2]=4,算出 13——可这相当于同时拿了值 2 和值 3,而它们相邻、本该互斥。正确写法是接 dp[v-2],跳过相邻那格。
⚠️ 容易写错的地方
✗ 错:total[v] 写成「出现次数」
✓ 对:total[v] = v × 出现次数(得分总和)
得的是分数总和,不是个数
✗ 错:dp 转移接了相邻的 dp[v-1]
✓ 对:选 v 时必须接 dp[v-2]
选 v 就排斥 v-1,不能再用 dp[v-1]
✗ 错:用 int 累加大数据
✓ 对:C++/Java 用 long
次数多、值大时总分会溢出 int
完整代码(Python / C++ / Java)
Python
class Solution:
def deleteAndEarn(self, nums):
if not nums:
return 0
mx = max(nums)
total = [0] * (mx + 1) # total[v] = v * 出现次数
for v in nums:
total[v] += v
prev2, prev1 = 0, 0 # dp[v-2], dp[v-1]
for v in range(mx + 1):
cur = max(prev1, prev2 + total[v]) # 选 or 不选
prev2, prev1 = prev1, cur
return prev1C++
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
if (nums.empty()) return 0;
int mx = *max_element(nums.begin(), nums.end());
vector<long long> total(mx + 1, 0); // total[v]=v*次数
for (int v : nums) total[v] += v;
long long prev2 = 0, prev1 = 0; // dp[v-2], dp[v-1]
for (int v = 0; v <= mx; v++) {
long long cur = max(prev1, prev2 + total[v]);
prev2 = prev1;
prev1 = cur;
}
return (int)prev1;
}
};Java
class Solution {
public int deleteAndEarn(int[] nums) {
if (nums.length == 0) return 0;
int mx = 0;
for (int v : nums) mx = Math.max(mx, v);
long[] total = new long[mx + 1]; // total[v]=v*次数
for (int v : nums) total[v] += v;
long prev2 = 0, prev1 = 0; // dp[v-2], dp[v-1]
for (int v = 0; v <= mx; v++) {
long cur = Math.max(prev1, prev2 + total[v]);
prev2 = prev1;
prev1 = cur;
}
return (int) prev1;
}
}复杂度
时间复杂度
O(n + M)
n=数组长度(统计 total),M=最大值(扫一遍值域填 dp),各一趟
空间复杂度
O(M)
total 数组按值域大小开;dp 用两个滚动变量 → O(1),合计 O(M)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 删除并获得点数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和打家劫舍到底什么关系?+
本质是同一道题。把值 0 到最大值排成一排,每个位置的权重是 sum[v],选了某位置就不能选左右相邻位置,求最大权重和,这就是打家劫舍不能偷相邻两家的模型。差别只在打家劫舍直接给你一排房子,这题要先按值把收益折成一排。
值域很大(比如到 10 亿,即 10^9)但元素很少怎么办?+
不开这么大的 sum 数组,改成排序去重后按值序处理。相邻两个值恰好相差 1 时才走「选了就跳一格」的排斥转移,相差大于 1 时两个值互不冲突,直接把收益累加。这样复杂度变成 O(n log n),和值域大小脱钩。
为什么不能贪心地每次选最大的值?+
贪心每次选最大值只看眼前收益,忽略了它会连带删光两边的值。碰上「选大就废小」的数据,比如值 4 的收益盖过值 3,选了 4 就丢掉 3 那三个数反而更少。动态规划把「到每个值的最优」记下来,让每步决策都建立在子问题已经算好的最优上,才不会被眼前的大数带偏。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 删除并获得点数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。