2530. Maximal Score After Applying K Operations
Problem Description
You start with a score of 0 and an array of integers nums
. You need to perform exactly k
operations to maximize your score.
In each operation, you:
- Choose any index
i
from the array (where0 <= i < nums.length
) - Add the value
nums[i]
to your score - Replace
nums[i]
withceil(nums[i] / 3)
(the ceiling of dividing by 3)
The ceiling function rounds up to the nearest integer. For example, ceil(10/3) = 4
and ceil(4/3) = 2
.
You can choose the same index multiple times across different operations. The goal is to find the maximum possible score after performing exactly k
operations.
In Example 1, with nums = [10,10,10,10,10]
and k = 5
, you can pick each element once, getting a score of 10 + 10 + 10 + 10 + 10 = 50
.
In Example 2, with nums = [1,10,3,3,3]
and k = 3
, the optimal strategy is:
- Pick index 1 (value 10): score increases by 10, array becomes
[1,4,3,3,3]
- Pick index 1 again (value 4): score increases by 4, array becomes
[1,2,3,3,3]
- Pick index 2 (value 3): score increases by 3, array becomes
[1,2,1,3,3]
- Total score:
10 + 4 + 3 = 17
The solution uses a max heap to always pick the largest available value at each step, ensuring the maximum possible score.
Intuition
The key insight is that we want to maximize our total score, which means we should always pick the largest available value at each step. This is a greedy approach - at each operation, we want to add the maximum possible value to our score.
Think about it this way: if we have values like [10, 3, 2]
, it's better to pick 10 first rather than 3 or 2, because we get more points immediately. Even though picking 10 will reduce it to ceil(10/3) = 4
, we've already secured those 10 points.
After each operation, the picked element gets reduced (divided by 3 and rounded up), but it might still be the largest element in the array. For instance, if we have [10, 3]
and pick 10, it becomes 4, giving us [4, 3]
. The element we just modified (4) is still larger than 3, so we should pick it again in the next operation.
This suggests we need a data structure that can:
- Quickly find the maximum element
- Remove the maximum element
- Insert a new element (the reduced value)
A max heap (priority queue) is perfect for this! It gives us O(log n)
operations for all three requirements. We can maintain all array elements in the heap, and at each step:
- Extract the maximum value
- Add it to our score
- Insert the reduced value
ceil(value/3)
back into the heap - Repeat for
k
operations
Since Python's heapq
is a min heap by default, we use negative values to simulate a max heap - the smallest negative number represents the largest positive number.
Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The solution implements a greedy algorithm using a max heap (priority queue) to always select the maximum element at each step.
Implementation Steps:
-
Initialize the Max Heap: Since Python's
heapq
module only provides a min heap, we simulate a max heap by negating all values. We convert the input arraynums
into negative values and heapify it:h = [-v for v in nums] heapify(h)
-
Initialize the Answer: Start with
ans = 0
to accumulate our score. -
Perform k Operations: For each of the
k
operations:- Extract the maximum element (which is the minimum negative value):
v = -heappop(h)
- Add this value to our total score:
ans += v
- Calculate the reduced value using
ceil(v / 3)
and push it back to the heap as a negative number:heappush(h, -(ceil(v / 3)))
- Extract the maximum element (which is the minimum negative value):
-
Return the Result: After
k
operations, return the accumulated score.
Time Complexity: O(k log n)
where n
is the length of the array. Each heap operation (pop and push) takes O(log n)
time, and we perform k
such operations.
Space Complexity: O(n)
for storing the heap.
The key insight is that by using a heap, we ensure that at each step we're always picking the largest available value, which guarantees the maximum possible total score after exactly k
operations.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with nums = [5, 8, 3]
and k = 4
operations.
Initial Setup:
- Create a max heap by negating values:
h = [-5, -8, -3]
- After heapifying:
h = [-8, -5, -3]
(min heap structure with -8 at root) - Score = 0
Operation 1:
- Extract max (pop -8): value = 8
- Add to score: score = 0 + 8 = 8
- Calculate reduced value: ceil(8/3) = 3
- Push -3 back to heap:
h = [-5, -3, -3]
Operation 2:
- Extract max (pop -5): value = 5
- Add to score: score = 8 + 5 = 13
- Calculate reduced value: ceil(5/3) = 2
- Push -2 back to heap:
h = [-3, -3, -2]
Operation 3:
- Extract max (pop -3): value = 3
- Add to score: score = 13 + 3 = 16
- Calculate reduced value: ceil(3/3) = 1
- Push -1 back to heap:
h = [-3, -2, -1]
Operation 4:
- Extract max (pop -3): value = 3
- Add to score: score = 16 + 3 = 19
- Calculate reduced value: ceil(3/3) = 1
- Push -1 back to heap:
h = [-2, -1, -1]
Final Result: Maximum score = 19
The greedy strategy ensures we always pick the largest available value. Notice how after reducing 8 to 3, it tied with the original 3 in the array, and we picked both of them in subsequent operations before picking the smaller values.
Solution Implementation
1from typing import List
2from heapq import heapify, heappop, heappush
3from math import ceil
4
5
6class Solution:
7 def maxKelements(self, nums: List[int], k: int) -> int:
8 # Create a max heap by negating all values (Python's heapq is min heap by default)
9 max_heap = [-num for num in nums]
10 heapify(max_heap)
11
12 total_score = 0
13
14 # Perform k operations
15 for _ in range(k):
16 # Pop the maximum element (negate to get original value)
17 max_value = -heappop(max_heap)
18
19 # Add the maximum value to the total score
20 total_score += max_value
21
22 # Calculate the new value after operation: ceil(value / 3)
23 new_value = ceil(max_value / 3)
24
25 # Push the new value back to the heap (negated for max heap)
26 heappush(max_heap, -new_value)
27
28 return total_score
29
1class Solution {
2 public long maxKelements(int[] nums, int k) {
3 // Create a max heap to always get the largest element efficiently
4 PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
5
6 // Add all elements from the array to the max heap
7 for (int num : nums) {
8 maxHeap.offer(num);
9 }
10
11 // Initialize the sum of selected elements
12 long totalSum = 0;
13
14 // Perform k operations
15 while (k-- > 0) {
16 // Extract the maximum element from the heap
17 int maxElement = maxHeap.poll();
18
19 // Add the maximum element to the total sum
20 totalSum += maxElement;
21
22 // Calculate ceiling of maxElement/3 and add it back to the heap
23 // Formula: ceil(x/3) = (x + 2) / 3 for positive integers
24 maxHeap.offer((maxElement + 2) / 3);
25 }
26
27 // Return the maximum sum after k operations
28 return totalSum;
29 }
30}
31
1class Solution {
2public:
3 long long maxKelements(vector<int>& nums, int k) {
4 // Create a max heap using all elements from nums
5 priority_queue<int> maxHeap(nums.begin(), nums.end());
6
7 // Initialize the result sum
8 long long totalSum = 0;
9
10 // Perform k operations
11 while (k--) {
12 // Extract the maximum element from the heap
13 int maxValue = maxHeap.top();
14 maxHeap.pop();
15
16 // Add the maximum value to our result
17 totalSum += maxValue;
18
19 // Calculate ceiling of maxValue/3 and push back to heap
20 // Formula: ceiling(a/b) = (a + b - 1) / b
21 // For b=3: ceiling(v/3) = (v + 2) / 3
22 maxHeap.push((maxValue + 2) / 3);
23 }
24
25 return totalSum;
26 }
27};
28
1/**
2 * Calculates the maximum sum by selecting k elements from the array,
3 * where after each selection, the selected element is replaced by ceil(element/3)
4 * @param nums - The input array of numbers
5 * @param k - The number of operations to perform
6 * @returns The maximum sum after k operations
7 */
8function maxKelements(nums: number[], k: number): number {
9 // Initialize a max priority queue to always get the largest element
10 const priorityQueue = new MaxPriorityQueue();
11
12 // Add all numbers from the input array to the priority queue
13 nums.forEach(num => priorityQueue.enqueue(num));
14
15 // Variable to store the accumulated sum
16 let totalSum = 0;
17
18 // Perform k operations
19 while (k > 0) {
20 // Extract the maximum element from the queue
21 const maxValue = priorityQueue.dequeue()!.element;
22
23 // Add the maximum value to the total sum
24 totalSum += maxValue;
25
26 // Calculate ceiling of maxValue/3 and add it back to the queue
27 // Formula: ceil(x/3) = floor((x+2)/3)
28 priorityQueue.enqueue(Math.floor((maxValue + 2) / 3));
29
30 // Decrement the operation counter
31 k--;
32 }
33
34 return totalSum;
35}
36
Time and Space Complexity
Time Complexity: O(n + k × log n)
- Building the initial max heap from the array takes
O(n)
time usingheapify()
- The main loop runs
k
iterations - In each iteration:
heappop()
operation takesO(log n)
timeheappush()
operation takesO(log n)
time
- Total time for the loop:
k × O(log n) = O(k × log n)
- Overall time complexity:
O(n) + O(k × log n) = O(n + k × log n)
Space Complexity: O(n)
- The heap
h
is created by storing alln
elements from the input array, requiringO(n)
additional space - No other significant auxiliary space is used
- Therefore, the space complexity is
O(n)
Note: The space complexity could be considered O(1)
if we modify the input array in-place instead of creating a new heap, but in this implementation, a new list is created for the heap.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division vs True Division
One of the most common mistakes is using integer division (//
) instead of true division (/
) when calculating the ceiling value.
Incorrect approach:
new_value = ceil(max_value // 3) # Wrong! Integer division happens first
Why it's wrong: The expression max_value // 3
performs integer division (floor division) first, then ceil()
is applied to an already truncated integer, which has no effect.
Example: For max_value = 10
:
- Wrong:
ceil(10 // 3)
=ceil(3)
=3
- Correct:
ceil(10 / 3)
=ceil(3.333...)
=4
Solution: Always use true division (/
) before applying ceil()
:
new_value = ceil(max_value / 3) # Correct
2. Forgetting to Negate Values for Max Heap
Python's heapq
module only provides a min heap. A common mistake is forgetting to negate values consistently when simulating a max heap.
Incorrect patterns:
# Forgetting to negate when pushing back heappush(max_heap, new_value) # Wrong! Should be -new_value # Forgetting to negate when popping max_value = heappop(max_heap) # Wrong! Should be -heappop(max_heap)
Solution: Always maintain consistency with negation:
- Negate when pushing:
heappush(max_heap, -new_value)
- Negate when popping:
max_value = -heappop(max_heap)
3. Manual Ceiling Calculation Error
Some might try to implement ceiling manually instead of using math.ceil()
, leading to errors:
Incorrect manual implementation:
new_value = (max_value + 2) // 3 # Works for positive integers but risky
Why it's problematic: While (n + 2) // 3
works for positive integers as a ceiling of n/3
, it's:
- Less readable and maintainable
- Prone to errors if the divisor changes
- May fail for edge cases or if the problem constraints change
Solution: Use the built-in math.ceil()
function for clarity and correctness:
from math import ceil new_value = ceil(max_value / 3)
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!