在问 · 算法题解

740. 删除并获得点数

中等动态规划值域计数 + 打家劫舍力扣原题 ↗

题目描述

输入整数数组 nums,初始得分为 0。

每次操作选出数组中的一个元素 x,把它移除并得到 x 分。随后数组中所有等于 x - 1 或 x + 1 的元素都要被移除,这些元素不计分。

可以进行任意多次操作。返回能得到的最高总分。

示例 1
输入:nums = [3,4,2]
输出:6

解释:取 4 得 4 分并清掉 3,再取 2 得 2 分,共 6 分。

示例 2
输入:nums = [2,2,3,3,3,4]
输出:9

解释:取 3 时清掉所有 2 和 4,再把三个 3 都取走,共 9 分。

数据范围

  • 1 <= nums.length <= 2 * 10^4
  • 1 <= nums[i] <= 10^4

解题思路:值域计数 + 打家劫舍

选了某个值 v,就应该把所有 v 都拿完,收益 total[v] = v × 出现次数;代价是 v-1 和 v+1 不能再选。于是问题变成在 total[1..m] 上“不能选相邻两个”的打家劫舍:用 take 表示选当前值的最优解,skip 表示不选当前值的最优解,每步 take = skip + total[v],skip = max(take, skip)。

为什么正确

处理到值 v 后,take / skip 是只考虑 1..v 时“选了 v / 没选 v”的最大得分。

复杂度分析

时间 O(n + m),空间 O(m),m 是最大值。

易错点

不要在原数组的下标上做打家劫舍,“相邻”指的是值相差 1。更新 take 时必须用上一步的 skip,所以要同时赋值。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def deleteAndEarn(self, nums: List[int]) -> int:
        m = max(nums)
        total = [0] * (m + 1)
        for x in nums:
            total[x] += x
        take, skip = 0, 0
        for v in range(1, m + 1):
            take, skip = skip + total[v], max(take, skip)
        return max(take, skip)

载入编辑器,自己跑一遍 →

其他解法

值域计数把问题直接变成打家劫舍,代码最短;排序分段只处理出现过的值,值域很大时更省时间和空间。

解法 2:排序 + 分段打家劫舍 时间 O(n log n),空间 O(n)

先排序,把相同的数合并成(值,总和)对。依次处理每个不同的值:如果它等于上一个值加 1,两者不能同时选,按打家劫舍转移 take = skip + 总和、skip = max(take, skip);否则两者互不影响,先把前面的最优值合并成 skip,再选当前值。

不依赖最大值 M,只和元素个数有关,适合数值很大但元素很少的情况。本题 M ≤ 10⁴,值域数组更简单。

class Solution:
    def deleteAndEarn(self, nums: List[int]) -> int:
        nums = sorted(nums)
        take = skip = 0
        prev = None
        i = 0
        while i < len(nums):
            v, s = nums[i], 0
            while i < len(nums) and nums[i] == v:
                s += v
                i += 1
            if prev is not None and v == prev + 1:
                take, skip = skip + s, max(take, skip)
            else:
                skip = max(take, skip)
                take = skip + s
            prev = v
        return max(take, skip)

动态规划 专题的其他题目

查看 动态规划 全部题目 →