2208. Minimum Operations to Halve Array Sum


Problem Description

The goal of this problem is to reduce the sum of a given array nums of positive integers to at least half of its original sum through a series of operations. In each operation, you can select any number from the array and reduce it exactly to half of its value. You are allowed to select reduced numbers in subsequent operations.

The objective is to determine the minimum number of such operations needed to achieve the goal of halving the array's sum.

Intuition

To solve this problem, a greedy approach is best, as taking the largest number and halving it will most significantly impact the sum in the shortest number of steps. Using a max heap data structure is suitable for efficiently retrieving and halving the largest number at each step.

The solution involves:

  1. Calculating the target sum, which is half the sum of the array nums.
  2. Creating a max heap to access the largest element quickly in each operation.
  3. Continuously extracting the largest element from the heap, halving it, and adding the halved value back to the heap.
  4. Accumulating the count of operations until the target sum is reached or passed.

This approach guarantees that each step is optimal in terms of reducing the total sum and reaching the goal using the fewest operations possible.

Learn more about Greedy and Heap (Priority Queue) patterns.

Solution Approach

The solution uses the following steps, algorithms, and data structures:

  1. Max Heap (Priority Queue): The Python code uses a max heap, which is implemented using a min heap with negated values (the heapq library in Python only provides a min heap). A max heap allows us to easily and efficiently access and remove the largest element in the array.

  2. Halving the Largest Element: At each step, the code pops the largest value in the heap, halves it, and pushes the negated half value back on to the heap. Since we're using a min heap, every value is negated when it's pushed onto the heap and negated again when it's popped off to maintain the original sign.

  3. Sum Reduction Tracking: We keep track of the current sum of nums we need to halve, starting with half of the original sum of nums. After halving and pushing the largest element back onto the heap, we decrement this sum by the halved value.

  4. Counting Operations: A variable ans is used to count the number of operations. It is incremented by one with each halving operation.

  5. Condition Check: The loop continues until the sum we are tracking is less than or equal to zero, meaning the total sum of the original array has been reduced by at least half.

In terms of algorithm complexity, this solution operates in O(n log n) time where n is the number of elements in nums. The log n factor comes from the push and pop operations of the heap, which occur for each of the n elements.

The code implementation based on these steps is shown in the reference solution with proper comments to highlight each step:

class Solution:
    def halveArray(self, nums: List[int]) -> int:
        s = sum(nums) / 2  # Target sum to achieve
        h = []  # Initial empty heap
        for v in nums:
            heappush(h, -v)  # Negate and push all values to the heap
        ans = 0  # Operation counter
        while s > 0:  # Continue until we've halved the array sum
            t = -heappop(h) / 2  # Extract and halve the largest value
            s -= t  # Reduce the target sum by the halved value
            heappush(h, -t)  # Push the negated halved value back onto the heap
            ans += 1  # Increment operation count
        return ans  # Return the total number of operations needed

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through an example to illustrate the solution approach.

Suppose we have the following array of numbers: nums = [10, 20, 30].

  1. Calculating the Target Sum:
    The sum of nums is 60, so halving the sum gives us a target of 30.

  2. Creating a Max Heap:
    We create a max heap with the negated values of nums: h = [-30, -20, -10].

  3. Reducing Sum with Operations:
    We now perform operations which will involve halving the largest element and keeping a tally of operations.

    • First Operation:
      The current sum we need to track is 30.
      Extract the largest element (-30) and halve its value (15).
      Push the negated halved value back onto the heap (-15).
      The heap is now h = [-20, -15, -10].
      Subtract 15 from the target sum, leaving us with 15.
      Increment the operation count (ans = 1).

    • Second Operation:
      Extract the largest element (-20) and halve its value (10).
      Push the negated halved value back onto the heap (-10).
      The heap is now h = [-15, -10, -10].
      Subtract 10 from the target sum, leaving us with 5.
      Increment the operation count (ans = 2).

    • Third Operation:
      Extract the largest element (-15) and halve its value (7.5).
      Push the negated halved value back onto the heap (-7.5).
      The heap is now h = [-10, -10, -7.5].
      Subtract 7.5 from the target sum, which would make it negative (-2.5).
      Increment the operation count (ans = 3).

    Since the sum we are tracking is now less than 0, the loop ends.

  4. Result:
    The minimum number of operations required to reduce the sum of nums to at least half its original sum is 3.

Solution Implementation

1from heapq import heappush, heappop
2
3class Solution:
4    def halveArray(self, nums: List[int]) -> int:
5        # Calculate the sum of half the elements to determine the target
6        target_sum = sum(nums) / 2
7      
8        # Initialize a max heap (using negative values because Python has a min heap by default)
9        max_heap = []
10      
11        # Add negative values of nums to the heap to simulate max heap
12        for value in nums:
13            heappush(max_heap, -value)
14      
15        # Counter for the number of operations performed
16        operations = 0
17      
18        # Keep reducing the target sum until it reaches 0 or below
19        while target_sum > 0:
20            # Retrieve and negate the largest element, then halve it
21            largest = -heappop(max_heap) / 2
22            # Subtract the halved value from the target sum
23            target_sum -= largest
24            # Push the halved negative value back to maintain the max heap
25            heappush(max_heap, -largest)
26            # Increment the operation count
27            operations += 1
28      
29        # Return the total number of operations needed to reach the target sum
30        return operations
31
1class Solution {
2
3    public int halveArray(int[] nums) {
4        // Initial sum of the array elements.
5        double sum = 0;
6        // Priority queue to store the array elements in descending order.
7        PriorityQueue<Double> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
8
9        // Add all elements to the priority queue and calculate the total sum.
10        for (int value : nums) {
11            maxHeap.offer((double) value);
12            sum += value;
13        }
14
15        // The target is to reduce the sum to less than half of its original value.
16        double halfSum = sum / 2.0;
17
18        // Counter for the number of operations performed.
19        int operations = 0;
20      
21        // Continue until we reduce the sum to less than half.
22        while (halfSum > 0) {
23            // Retrieve and remove the largest element from the queue.
24            double largest = maxHeap.poll();
25            // Divide it by 2 (halving the element) and subtract from halfSum.
26            halfSum -= largest / 2.0;
27            // Add the halved element back to the priority queue.
28            maxHeap.offer(largest / 2.0);
29            // Increment the operation counter.
30            operations++;
31        }
32
33        // Return the number of operations required to achieve the target.
34        return operations;
35    }
36}
37
1#include <vector>
2#include <queue>
3using namespace std;
4
5class Solution {
6public:
7    // Function to find the minimum number of operations to reduce array sum to less than or equal to half of the initial sum.
8    int halveArray(vector<int>& nums) {
9        // Use a max heap to keep track of the largest numbers in the array 
10        priority_queue<double> maxHeap;
11      
12        double total = 0; // Original total sum of the array elements
13        for (int value : nums) {
14            total += value; // Accumulate total sum
15            maxHeap.push(value); // Add current value to the max heap
16        }
17      
18        double targetHalf = total / 2.0; // Our target is to reduce the total to this value or less
19        int operations = 0; // Initialize the number of operations performed to 0
20      
21        // Continue reducing the total sum until it's less than or equal to targetHalf
22        while (total > targetHalf) {
23            double topValue = maxHeap.top() / 2.0; // Halve the largest number in max heap
24            maxHeap.pop(); // Remove this largest number from max heap
25            total -= topValue; // Subtract the halved value from the total sum
26            maxHeap.push(topValue); // Push the halved value back into max heap
27            operations++; // Increment the number of operations
28        }
29      
30        return operations; // Return the total number of operations performed
31    }
32};
33
1// Importing PriorityQueue to use in the implementation
2import { MaxPriorityQueue } from 'typescript-collections';
3
4// This function takes an array of numbers and returns the minimum number of
5// operations to reduce the sum of the array to less than or equal to half its original sum by performing
6// operations that halve the value of any element in the array.
7function halveArray(nums: number[]): number {
8    // Calculate the target sum which is half the sum of the input array
9    let targetSum: number = nums.reduce((accumulator, currentValue) => accumulator + currentValue) / 2;
10  
11    // Initialize a max priority queue to facilitate the retrieval of the largest element
12    const maxPriorityQueue = new MaxPriorityQueue<number>();
13  
14    // Enqueue all numbers in the array into the max priority queue with their values as priorities
15    for (const value of nums) {
16        maxPriorityQueue.enqueue(value, value);
17    }
18  
19    // Initialize the operation counter
20    let operationCount: number = 0;
21  
22    // Continue until the remaining sum is reduced to targetSum or less
23    while (targetSum > 0) {
24        // Dequeue the largest element
25        let dequeuedItem = maxPriorityQueue.dequeue().value;
26      
27        // Halve the dequeued element
28        dequeuedItem /= 2;
29      
30        // Subtract the halved value from the remaining sum
31        targetSum -= dequeuedItem;
32      
33        // Re-enqueue the halved element to ensure correct ordering in the priority queue
34        maxPriorityQueue.enqueue(dequeuedItem, dequeuedItem);
35      
36        // Increment the operation counter
37        operationCount += 1;
38    }
39  
40    // Return the total number of operations performed
41    return operationCount;
42}
43
44// Example usage:
45// const result = halveArray([10, 20, 7]);
46// console.log(result); // Outputs the number of operations needed
47

Time and Space Complexity

Time Complexity

The given algorithm consists of multiple steps that contribute to the overall time complexity:

  1. Sum calculation: The sum of the array is calculated with a complexity of O(n), where n is the number of elements in nums.

  2. Heap construction: Inserting all elements into a heap has an overall complexity of O(n * log(n)) as each insertion operation into the heap is O(log(n)), and it is performed n times for n elements.

  3. Halving elements until sum is reduced: The complexity of this part depends on the number of operations we need to reduce the sum by half. In the worst-case scenario, every element is divided multiple times. For each halving operation, we remove the maximum element (O(log(n)) complexity for removal) and insert it back into the heap (O(log(n)) complexity for insertion). The number of such operations could vary, but it could potentially be O(m * log(n)), where m is the number of halving operations needed.

Therefore, the time complexity of the algorithm is determined by summing these complexities:

  • Sum computation: O(n)
  • Heapification: O(n * log(n))
  • Halving operations: O(m * log(n))

Since the number of halving operations m is not necessarily linear and depends on the values in the array, we cannot directly relate it to n. As a result, the overall worst-case time complexity of the algorithm is O(n * log(n) + m * log(n)).

Space Complexity

The space complexity of the algorithm is determined by:

  1. Heap storage: We store all n elements in the heap, which requires O(n) space.

  2. Auxiliary space: Aside from the heap, the algorithm uses a constant amount of extra space for variables like s, t, and ans.

Hence, the space complexity is O(n).

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

How does quick sort divide the problem into subproblems?


Recommended Readings

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