2208. Minimum Operations to Halve Array Sum
Problem Description
You have an array nums
containing positive integers. You can perform operations on this array where each operation consists of:
- Selecting any number from the array
- Reducing that number to exactly half of its current value
- The halved number remains in the array and can be selected for future operations
Your goal is to find the minimum number of operations needed to reduce the total sum of the array by at least half of its original sum.
For example, if the original sum of the array is 20, you need to perform operations until the sum becomes 10 or less.
The key insight is that you're allowed to repeatedly halve the same number multiple times. Each time you halve a number, it counts as one operation, and the halved value replaces the original value in the array for potential future operations.
The solution uses a greedy approach with a max heap. Since we want to minimize the number of operations, we should always halve the largest number in the array at each step, as this gives us the maximum reduction per operation. The algorithm:
- Calculates half of the original sum (
s = sum(nums) / 2
) as our target reduction - Uses a max heap (implemented with negative values in Python's min heap) to track array elements
- Repeatedly extracts the maximum value, halves it, adds it back to the heap, and decrements
s
by the amount reduced - Continues until
s ≤ 0
, meaning we've reduced the sum by at least half - Returns the count of operations performed
Intuition
To minimize the number of operations, we need to maximize the reduction in sum with each operation. Since each operation halves a number, the larger the number we choose to halve, the greater the reduction we achieve.
Think about it this way: if we have numbers [10, 2, 1]
, halving 10 gives us a reduction of 5, while halving 2 only gives us a reduction of 1. Clearly, halving the larger number is more efficient.
This leads us to a greedy strategy: always halve the current largest number in the array. But here's the catch - after halving a number, it becomes smaller and might no longer be the largest. For instance, if we have [10, 8]
and halve 10 to get 5, our array becomes [5, 8]
. Now 8 is the largest, so we should halve it next.
This dynamic nature of "what's the current largest?" suggests we need a data structure that can efficiently:
- Find the maximum element
- Remove it
- Insert the halved value back
- Repeat until we've reduced the sum by at least half
A max heap (priority queue) is perfect for this because it maintains the maximum element at the top with O(log n)
insertion and extraction operations.
The target is to reduce the sum by at least half, so we track how much we still need to reduce (s = original_sum / 2
). Each time we halve a number x
, we reduce the sum by x/2
, so we subtract this from s
. When s
becomes zero or negative, we've achieved our goal.
Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The implementation follows the greedy strategy with a max heap to always halve the largest available number:
Step 1: Calculate Target Reduction
s = sum(nums) / 2
We calculate half of the original sum. This represents how much we need to reduce from the array's total.
Step 2: Initialize Max Heap
pq = [] for x in nums: heappush(pq, -x)
Python's heapq
module only provides a min heap, so we use a common trick: store negative values to simulate a max heap. When we push -x
, the largest positive number becomes the smallest negative number, sitting at the heap's root.
Step 3: Greedy Reduction Process
ans = 0 while s > 0: t = -heappop(pq) / 2 s -= t heappush(pq, -t) ans += 1
The main loop continues while we still need to reduce the sum (s > 0
):
- Extract the maximum value:
-heappop(pq)
gives us the largest positive number - Calculate the reduction:
t = max_value / 2
represents how much we're reducing - Update our target:
s -= t
decreases the remaining amount we need to reduce - Put the halved value back:
heappush(pq, -t)
adds the halved number back to the heap (negated for max heap behavior) - Count the operation:
ans += 1
Time Complexity: O(n + k log n)
where n
is the array size and k
is the number of operations. Building the initial heap takes O(n)
, and each of the k
operations involves heap operations taking O(log n)
.
Space Complexity: O(n)
for storing the heap with all array elements.
The algorithm guarantees finding the minimum operations because at each step, we make the locally optimal choice (halving the largest number), which leads to the globally optimal solution.
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 concrete example with nums = [5, 19, 8, 1]
.
Initial Setup:
- Original sum = 5 + 19 + 8 + 1 = 33
- Target reduction needed: s = 33 / 2 = 16.5
- Build max heap (using negatives):
[-19, -8, -5, -1]
Operation 1:
- Extract max: 19 (from -19)
- Halve it: 19 / 2 = 9.5
- Reduction achieved: 9.5
- Update s: 16.5 - 9.5 = 7
- Insert 9.5 back: heap becomes
[-9.5, -8, -5, -1]
- Array conceptually:
[5, 9.5, 8, 1]
Operation 2:
- Extract max: 9.5
- Halve it: 9.5 / 2 = 4.75
- Reduction achieved: 4.75
- Update s: 7 - 4.75 = 2.25
- Insert 4.75 back: heap becomes
[-8, -5, -4.75, -1]
- Array conceptually:
[5, 4.75, 8, 1]
Operation 3:
- Extract max: 8
- Halve it: 8 / 2 = 4
- Reduction achieved: 4
- Update s: 2.25 - 4 = -1.75
- Since s ≤ 0, we've achieved our goal!
Result: 3 operations needed
The final sum would be 5 + 4.75 + 4 + 1 = 14.75, which is less than half of the original sum (16.5). By always choosing the largest number to halve, we minimized the number of operations needed.
Solution Implementation
1class Solution:
2 def halveArray(self, nums: List[int]) -> int:
3 # Calculate half of the total sum (target reduction)
4 target_reduction = sum(nums) / 2
5
6 # Create a max heap (using negative values since Python has min heap by default)
7 max_heap = []
8 for num in nums:
9 heappush(max_heap, -num)
10
11 # Track number of operations performed
12 operations_count = 0
13
14 # Keep reducing until we've reduced by at least half of the original sum
15 while target_reduction > 0:
16 # Extract the largest element and halve it
17 largest_element = -heappop(max_heap)
18 halved_value = largest_element / 2
19
20 # Reduce our target by the amount we just removed
21 target_reduction -= halved_value
22
23 # Push the halved value back into the heap
24 heappush(max_heap, -halved_value)
25
26 # Increment operation counter
27 operations_count += 1
28
29 return operations_count
30
1class Solution {
2 public int halveArray(int[] nums) {
3 // Create a max heap to always get the largest element
4 PriorityQueue<Double> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
5
6 // Calculate the total sum and add all elements to the heap
7 double totalSum = 0;
8 for (int num : nums) {
9 totalSum += num;
10 maxHeap.offer((double) num);
11 }
12
13 // Target is to reduce the sum by at least half
14 double targetReduction = totalSum / 2.0;
15
16 // Count the number of operations needed
17 int operationCount = 0;
18
19 // Keep reducing until we've reduced by at least half of the original sum
20 while (targetReduction > 0) {
21 // Get the largest element and halve it
22 double largest = maxHeap.poll();
23 double halvedValue = largest / 2.0;
24
25 // Reduce our target by the amount we just removed
26 targetReduction -= halvedValue;
27
28 // Put the halved value back into the heap
29 maxHeap.offer(halvedValue);
30
31 // Increment the operation counter
32 operationCount++;
33 }
34
35 return operationCount;
36 }
37}
38
1class Solution {
2public:
3 int halveArray(vector<int>& nums) {
4 // Use max heap to always get the largest element
5 priority_queue<double> maxHeap;
6
7 // Calculate total sum and populate the heap
8 double totalSum = 0;
9 for (int num : nums) {
10 totalSum += num;
11 maxHeap.push(static_cast<double>(num));
12 }
13
14 // Target is to reduce sum by half
15 double targetReduction = totalSum / 2.0;
16
17 // Count number of operations needed
18 int operationCount = 0;
19
20 // Keep halving the maximum element until we've reduced by at least half
21 while (targetReduction > 0) {
22 // Get the current maximum element
23 double currentMax = maxHeap.top();
24 maxHeap.pop();
25
26 // Halve it
27 double halvedValue = currentMax / 2.0;
28
29 // Subtract the reduction from our target
30 targetReduction -= halvedValue;
31
32 // Push the halved value back to heap
33 maxHeap.push(halvedValue);
34
35 // Increment operation count
36 ++operationCount;
37 }
38
39 return operationCount;
40 }
41};
42
1/**
2 * Finds the minimum number of operations to reduce the array sum by at least half
3 * Each operation halves a selected element from the array
4 * @param nums - Array of positive numbers to process
5 * @returns Minimum number of operations needed
6 */
7function halveArray(nums: number[]): number {
8 // Calculate half of the total sum as our target reduction
9 let targetReduction: number = nums.reduce((accumulator, current) => accumulator + current) / 2;
10
11 // Initialize a max priority queue to always process the largest element
12 const maxHeap = new MaxPriorityQueue();
13
14 // Add all numbers to the priority queue with their value as priority
15 for (const num of nums) {
16 maxHeap.enqueue(num, num);
17 }
18
19 // Track the number of operations performed
20 let operationCount: number = 0;
21
22 // Continue halving elements until we've reduced the sum by at least half
23 while (targetReduction > 0) {
24 // Get the largest element and halve it
25 const halvedValue: number = maxHeap.dequeue().element / 2;
26
27 // Subtract the halved amount from our target reduction
28 targetReduction -= halvedValue;
29
30 // Increment operation counter
31 operationCount++;
32
33 // Put the halved value back into the queue for potential future operations
34 maxHeap.enqueue(halvedValue, halvedValue);
35 }
36
37 return operationCount;
38}
39
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity consists of:
- Building the initial heap by pushing all
n
elements:O(n × log n)
since eachheappush
operation takesO(log n)
time - The while loop performs operations until the sum is reduced by at least half. In the worst case, we might need
O(n × log n)
operations total:- Each iteration performs one
heappop
operation:O(log n)
- Each iteration performs one
heappush
operation:O(log n)
- The maximum number of iterations is bounded by
O(n × log n)
because each operation halves a value, and in the worst case scenario (repeatedly halving the largest values), the number of operations needed is logarithmic in the ratio between the largest and smallest meaningful values
- Each iteration performs one
Space Complexity: O(n)
The space complexity is determined by:
- The priority queue
pq
stores alln
elements from the input array:O(n)
- Additional variables (
s
,ans
,t
) use constant space:O(1)
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division Instead of Float Division
Pitfall: Using integer division (//
) instead of float division (/
) when calculating the target reduction or halving values. This can lead to incorrect results due to precision loss.
# WRONG - loses precision
target_reduction = sum(nums) // 2 # Integer division
halved_value = largest_element // 2 # Integer division
# CORRECT - maintains precision
target_reduction = sum(nums) / 2 # Float division
halved_value = largest_element / 2 # Float division
2. Forgetting to Negate Values for Max Heap
Pitfall: Since Python's heapq only provides a min heap, forgetting to negate values when pushing or popping can result in processing the smallest values instead of the largest ones.
# WRONG - treats it as min heap heappush(max_heap, num) # Pushes positive value largest = heappop(max_heap) # Gets smallest value # CORRECT - simulates max heap heappush(max_heap, -num) # Pushes negative value largest = -heappop(max_heap) # Negates to get largest positive value
3. Not Putting Halved Value Back in Heap
Pitfall: After halving a number, forgetting to add it back to the heap. The problem allows selecting the same (now halved) number for future operations.
# WRONG - doesn't add halved value back while target_reduction > 0: largest = -heappop(max_heap) target_reduction -= largest / 2 # Missing: heappush(max_heap, -largest/2) operations_count += 1 # CORRECT - adds halved value back for potential future operations while target_reduction > 0: largest = -heappop(max_heap) halved = largest / 2 target_reduction -= halved heappush(max_heap, -halved) # Put it back in heap operations_count += 1
4. Incorrect Target Calculation
Pitfall: Misunderstanding what "reduce by half" means. The target should be half of the original sum, not the final sum we want to achieve.
# WRONG - tries to reach half the sum as final value
current_sum = sum(nums)
target_final_sum = current_sum / 2
while current_sum > target_final_sum:
# Complex tracking of current sum
# CORRECT - tracks how much to reduce
target_reduction = sum(nums) / 2
while target_reduction > 0:
# Simply subtract each reduction from target
5. Using Fixed Array Instead of Heap
Pitfall: Trying to use a sorted array and re-sorting after each operation, which is inefficient.
# WRONG - O(n log n) per operation due to sorting nums.sort(reverse=True) while target_reduction > 0: nums[0] = nums[0] / 2 target_reduction -= nums[0] nums.sort(reverse=True) # Expensive re-sort operations_count += 1 # CORRECT - O(log n) per operation with heap # Use heap operations as shown in the solution
Which data structure is used to implement recursion?
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!