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:

  1. Choose any index i from the array (where 0 <= i < nums.length)
  2. Add the value nums[i] to your score
  3. Replace nums[i] with ceil(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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Quickly find the maximum element
  2. Remove the maximum element
  3. 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 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

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:

  1. 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 array nums into negative values and heapify it:

    h = [-v for v in nums] heapify(h)
  2. Initialize the Answer: Start with ans = 0 to accumulate our score.

  3. 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)))
  4. 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 Evaluator

Example 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.

Time and Space Complexity

Time Complexity: O(n + k × log n)

  • Building the initial max heap from the array takes O(n) time using heapify()
  • The main loop runs k iterations
  • In each iteration:
    • heappop() operation takes O(log n) time
    • heappush() operation takes O(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 all n elements from the input array, requiring O(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)
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More