740. Delete And Earn - Explanation
Description
Choose a value from nums, remove one occurrence, and earn that many points. The operation also removes every occurrence of the values one smaller and one larger, without earning points for them. Repeat as desired and return the greatest total score.
Example 1:
Input: nums = [1,1,2,3]
Output: 5Explanation: Choose both occurrences of 1 and the value 3, earning 1 + 1 + 3 = 5. Choosing 1 removes the 2, but does not remove the 3.
Example 2:
Input: nums = [2,4,6]
Output: 12Explanation: No two values differ by 1, so all can be chosen. The score is 2 + 4 + 6 = 12.
Example 3:
Input: nums = [5]
Output: 5Explanation: Choose the only entry to earn 5 points.
Constraints:
1 <= nums.length <= 200001 <= nums[i] <= 10000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Understanding how to build solutions from subproblems, similar to House Robber pattern
- Hash Maps - Grouping and summing values by key to precompute totals for each unique number
- Sorting - Ordering elements to process consecutive values together
1. Recursion
Intuition
When you pick a number x, you earn all points from every occurrence of x, but you must delete all instances of x - 1 and x + 1. This creates a choice at each distinct value: either take it (and skip the next consecutive value) or skip it. Sorting helps group identical values together, making it easy to sum all occurrences. We recursively explore both choices at each group of identical numbers.
Algorithm
- Sort the array to group identical numbers together.
- Define a recursive function
dfs(i)starting at indexi:- If
iis out of bounds, return0. - Sum all occurrences of
nums[i]and moveipast this group. - Compute the result of skipping this group:
dfs(new_i). - Skip any numbers equal to
nums[i] + 1(they would be deleted if we pick). - Compute the result of picking this group:
pick + dfs(after_skipping). - Return the maximum of picking vs skipping.
- If
- Call
dfs(0)and return the result.
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
nums.sort()
def dfs(i):
if i >= len(nums):
return 0
cur = nums[i]
pick = 0
while i < len(nums) and nums[i] == cur:
pick += nums[i]
i += 1
res = dfs(i)
while i < len(nums) and nums[i] == 1 + cur:
i += 1
res = max(res, pick + dfs(i))
return res
return dfs(0)Time & Space Complexity
- Time complexity:
- Space complexity:
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems since we may revisit the same index multiple times. By precomputing the total value for each unique number (sum of all occurrences) and using memoization, we avoid redundant calculations. The problem reduces to: for each unique number, decide whether to take it (earning its total value but skipping the next consecutive number) or skip it.
Algorithm
- Build a map where each unique number maps to the sum of all its occurrences.
- Extract and sort the unique numbers.
- Create a memo array initialized to
-1. - Define
dfs(i):- If
iis out of bounds, return0. - If
memo[i]is set, return it. - Compute the value of taking
nums[i]: its total value plusdfs(i + 2)if the next number is consecutive, elsedfs(i + 1). - The result is
max(take, dfs(i + 1)). - Store in memo and return.
- If
- Return
dfs(0).
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
val = defaultdict(int)
for num in nums:
val[num] += num
nums = sorted(list(set(nums)))
memo = [-1] * len(nums)
def dfs(i):
if i >= len(nums):
return 0
if memo[i] != -1:
return memo[i]
res = val[nums[i]]
if i + 1 < len(nums) and nums[i] + 1 == nums[i + 1]:
res += dfs(i + 2)
else:
res += dfs(i + 1)
res = max(res, dfs(i + 1))
memo[i] = res
return res
return dfs(0)Time & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up) - I
Intuition
We can convert the top-down solution to bottom-up by processing unique numbers from right to left. At each position, we compute the maximum points achievable from that position onward. If the current and next numbers are consecutive, taking the current means skipping the next; otherwise, we can take the current and continue from the next.
Algorithm
- Build a map of each unique number to the sum of all its occurrences.
- Extract and sort the unique numbers.
- Create a DP array of size
n + 1initialized to0. - Iterate from the last unique number to the first:
- Compute
takeas the value of the current number plusdp[i + 2]if the next is consecutive, elsedp[i + 1]. - Set
dp[i] = max(dp[i + 1], take).
- Compute
- Return
dp[0].
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
val = defaultdict(int)
for num in nums:
val[num] += num
nums = sorted(list(set(nums)))
dp = [0] * (len(nums) + 1)
for i in range(len(nums) - 1, -1, -1):
take = val[nums[i]]
if i + 1 < len(nums) and nums[i + 1] == nums[i] + 1:
take += dp[i + 2]
else:
take += dp[i + 1]
dp[i] = max(dp[i + 1], take)
return dp[0]Time & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Bottom-Up) - II
Intuition
Instead of working with unique sorted numbers, we can use an array indexed by the numbers themselves (0 to max). Each index stores the total points for that number. This transforms the problem into the classic House Robber problem: you cannot take adjacent indices. We iterate backward, and at each position, choose the maximum of skipping it or taking it plus the result two positions ahead.
Algorithm
- Find the maximum value
min the array. - Create a DP array of size
m + 2initialized to0. - For each number in the input, add it to
dp[num](accumulating total points per number). - Iterate from
m - 1down to1:- Set
dp[i] = max(dp[i + 1], dp[i + 2] + dp[i]).
- Set
- Return
dp[1].
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the maximum element in the array and is the size of the array.
5. Dynamic Programming (Space Optimized)
Intuition
We only need to track the maximum earnings for the previous two positions, similar to the space-optimized House Robber solution. By iterating through sorted unique numbers and maintaining two variables, we can reduce space complexity. When consecutive numbers differ by more than 1, there is no conflict, so we can add the current earnings directly.
Algorithm
- Build a map of each unique number to the sum of all its occurrences.
- Sort the unique numbers.
- Initialize
earn1 = 0andearn2 = 0to track the max earnings at the previous two positions. - Iterate through the sorted unique numbers:
- If the current number is consecutive to the previous, we have a choice:
earn2 = max(curEarn + earn1, earn2). - If not consecutive, we can freely take the current:
earn2 = curEarn + earn2. - Update
earn1to the oldearn2before modifying.
- If the current number is consecutive to the previous, we have a choice:
- Return
earn2.
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
count = Counter(nums)
nums = sorted(list(set(nums)))
earn1, earn2 = 0, 0
for i in range(len(nums)):
curEarn = nums[i] * count[nums[i]]
if i > 0 and nums[i] == nums[i - 1] + 1:
temp = earn2
earn2 = max(curEarn + earn1, earn2)
earn1 = temp
else:
temp = earn2
earn2 = curEarn + earn2
earn1 = temp
return earn2Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Not Summing All Occurrences
When you pick a number, you earn points from every occurrence of that number in the array. A common mistake is only counting it once instead of summing all instances.
# Wrong: only counts one occurrence
val[num] = num
# Correct: sum all occurrences
val[num] += numForgetting to Skip Adjacent Values
When you earn points for a number x, you must delete all instances of x-1 and x+1. This means if you pick a value, you cannot pick the consecutive values. Missing this skip logic leads to incorrect results.
# Wrong: doesn't skip consecutive numbers
res = val[nums[i]] + dfs(i + 1)
# Correct: skip next if consecutive
if nums[i] + 1 == nums[i + 1]:
res = val[nums[i]] + dfs(i + 2)
else:
res = val[nums[i]] + dfs(i + 1)Treating Non-Consecutive Numbers as Adjacent
In the House Robber transformation, only consecutive numbers conflict. If numbers have gaps (e.g., 2 and 5), both can be taken without any penalty. Treating all numbers as adjacent loses points.
# Wrong: always treats as adjacent
earn2 = max(curEarn + earn1, earn2)
# Correct: check if actually consecutive
if i > 0 and nums[i] == nums[i - 1] + 1:
earn2 = max(curEarn + earn1, earn2)
else:
earn2 = curEarn + earn2 # No conflict, add freely
Sign in to join the discussion