Facebook Pixel

2439. Minimize Maximum of Array

Problem Description

You have an array nums with n non-negative integers (0-indexed).

You can perform the following operation any number of times:

  • Pick an index i where 1 <= i < n and nums[i] > 0
  • Decrease nums[i] by 1
  • Increase nums[i-1] by 1

This operation essentially moves one unit from position i to position i-1 (shifting value leftward).

Your goal is to minimize the maximum value in the array after performing any number of these operations. Return this minimum possible maximum value.

For example, if you have nums = [3, 7, 1, 6], you can perform operations to redistribute values leftward to minimize the largest element in the resulting array.

The key insight is that you can only move values leftward (from higher indices to lower indices), never rightward. This means each element can potentially receive contributions from elements to its right, but can only give to the element immediately to its left.

The solution uses binary search to find the minimum possible maximum value. For each candidate maximum value mx, it checks whether it's achievable by simulating the process backwards - starting from the rightmost element and calculating how much excess needs to be pushed left if we want to keep all elements at most mx. If the first element (which cannot push anything further left) ends up exceeding mx after receiving all the excess, then mx is not achievable.

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

Intuition

The first observation is that we're trying to minimize the maximum value, which suggests binary search might be useful. Why? Because if we can achieve a maximum value of mx, then we can also achieve any value greater than mx. This monotonic property makes binary search applicable.

The second key insight is understanding what the operations actually do. Since we can only move values from right to left (from index i to i-1), think of it like water flowing downhill from right to left. Each position can receive "water" from positions to its right and pass it further left.

Now, how do we check if a certain maximum value mx is achievable? We need to verify that we can redistribute the array values such that no element exceeds mx.

Working backwards from right to left makes sense here. For each element nums[i], if it exceeds mx, we need to push the excess (nums[i] - mx) to the left. This excess accumulates as we move left. When we reach nums[0], it has nowhere to push its excess - it must absorb everything. If nums[0] plus all the accumulated excess is still <= mx, then mx is achievable.

Why does this greedy approach work? Because we're forced to handle all excess - we can't leave any element above mx. By processing from right to left and pushing all excess leftward, we're essentially finding the minimum burden on nums[0]. If even this minimum burden makes nums[0] exceed mx, then mx is impossible to achieve.

The binary search range is from 0 to max(nums) because:

  • The minimum possible maximum is at least 0 (or could be the average for a tighter bound)
  • The maximum possible maximum is the current maximum in the array (we can't increase it)

Learn more about Greedy, Binary Search, Dynamic Programming and Prefix Sum patterns.

Solution Approach

The solution uses binary search to find the minimum possible maximum value. Here's how the implementation works:

Binary Search Setup:

  • Initialize left = 0 and right = max(nums) as the search boundaries
  • The answer must lie within this range since we can't make the maximum smaller than 0 or larger than the current maximum

Check Function: The check(mx) function determines if we can achieve a maximum value of mx:

  1. Initialize a deficit variable d = 0 to track accumulated excess that needs to be pushed left
  2. Iterate through the array from right to left (using nums[:0:-1] which reverses the array except the first element)
  3. For each element x:
    • Calculate the total burden: d + x
    • If this exceeds mx, update deficit: d = max(0, d + x - mx)
    • The max(0, ...) ensures we don't have negative deficit
  4. Finally, check if nums[0] + d <= mx
    • nums[0] must absorb all the accumulated deficit d
    • If the sum doesn't exceed mx, then mx is achievable

Binary Search Logic:

while left < right:
    mid = (left + right) >> 1  # Equivalent to (left + right) // 2
    if check(mid):
        right = mid  # mid is achievable, try smaller values
    else:
        left = mid + 1  # mid is not achievable, need larger values

The binary search finds the smallest mx where check(mx) returns True.

Why this works:

  • When we process from right to left, we greedily push as much excess as possible to the left
  • Each position can only reduce its value to at most mx, any excess must go left
  • The first element nums[0] becomes the final accumulator - it has no left neighbor to pass excess to
  • If even with optimal redistribution nums[0] exceeds mx, then mx is impossible

Time Complexity: O(n * log(max(nums))) where n is the array length

  • Binary search runs O(log(max(nums))) iterations
  • Each check function takes O(n) time

Space Complexity: O(1) - only using a few variables

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 the solution with nums = [3, 7, 1, 6].

Step 1: Binary Search Setup

  • left = 0, right = max(nums) = 7
  • We'll search for the minimum possible maximum value

Step 2: First Binary Search Iteration

  • mid = (0 + 7) // 2 = 3

  • Check if maximum value of 3 is achievable:

    Check(3) Process (right to left):

    • Start with deficit d = 0
    • Process nums[3] = 6: Total burden = 0 + 6 = 6
      • Exceeds 3, so deficit = 6 - 3 = 3
    • Process nums[2] = 1: Total burden = 3 + 1 = 4
      • Exceeds 3, so deficit = 4 - 3 = 1
    • Process nums[1] = 7: Total burden = 1 + 7 = 8
      • Exceeds 3, so deficit = 8 - 3 = 5
    • Check nums[0]: 3 + 5 = 8 > 3

    Since check(3) = False, we need a larger value: left = 4

Step 3: Second Binary Search Iteration

  • mid = (4 + 7) // 2 = 5

  • Check if maximum value of 5 is achievable:

    Check(5) Process:

    • Start with deficit d = 0
    • Process nums[3] = 6: Total burden = 0 + 6 = 6
      • Exceeds 5, so deficit = 6 - 5 = 1
    • Process nums[2] = 1: Total burden = 1 + 1 = 2
      • Doesn't exceed 5, so deficit = 0
    • Process nums[1] = 7: Total burden = 0 + 7 = 7
      • Exceeds 5, so deficit = 7 - 5 = 2
    • Check nums[0]: 3 + 2 = 5 ≤ 5

    Since check(5) = True, we can try smaller: right = 5

Step 4: Third Binary Search Iteration

  • mid = (4 + 5) // 2 = 4

  • Check if maximum value of 4 is achievable:

    Check(4) Process:

    • Start with deficit d = 0
    • Process nums[3] = 6: Total burden = 0 + 6 = 6
      • Exceeds 4, so deficit = 6 - 4 = 2
    • Process nums[2] = 1: Total burden = 2 + 1 = 3
      • Doesn't exceed 4, so deficit = 0
    • Process nums[1] = 7: Total burden = 0 + 7 = 7
      • Exceeds 4, so deficit = 7 - 4 = 3
    • Check nums[0]: 3 + 3 = 6 > 4

    Since check(4) = False: left = 5

Step 5: Termination

  • left = right = 5
  • Answer: 5

The final redistributed array would be [5, 5, 3, 4] after operations:

  • Move 2 from index 3 to 2: [3, 7, 3, 4]
  • Move 2 from index 1 to 0: [5, 5, 3, 4]

Solution Implementation

1class Solution:
2    def minimizeArrayValue(self, nums: List[int]) -> int:
3        """
4        Find the minimum possible maximum value in the array after operations.
5        Operations allow moving value from nums[i] to nums[i-1] for i > 0.
6      
7        Args:
8            nums: List of integers to process
9          
10        Returns:
11            The minimum possible maximum value after all operations
12        """
13      
14        def can_achieve_max(target_max: int) -> bool:
15            """
16            Check if we can make all elements <= target_max by moving values left.
17          
18            We process from right to left, tracking excess that needs to be moved.
19            If an element exceeds target_max, we must move the excess to the left.
20          
21            Args:
22                target_max: The maximum value we're trying to achieve
23              
24            Returns:
25                True if achievable, False otherwise
26            """
27            excess_to_move = 0
28          
29            # Process array from right to left (excluding the first element)
30            for current_value in nums[:0:-1]:
31                # Calculate how much needs to be moved left from current position
32                excess_to_move = max(0, excess_to_move + current_value - target_max)
33          
34            # Check if first element can absorb all excess without exceeding target_max
35            return nums[0] + excess_to_move <= target_max
36      
37        # Binary search for the minimum achievable maximum value
38        left_bound, right_bound = 0, max(nums)
39      
40        while left_bound < right_bound:
41            mid_point = (left_bound + right_bound) >> 1  # Equivalent to // 2
42          
43            if can_achieve_max(mid_point):
44                # If achievable, try for a smaller maximum
45                right_bound = mid_point
46            else:
47                # If not achievable, need a larger maximum
48                left_bound = mid_point + 1
49      
50        return left_bound
51
1class Solution {
2    private int[] nums;
3
4    /**
5     * Minimizes the maximum value in the array after performing operations.
6     * Uses binary search to find the minimum possible maximum value.
7     * 
8     * @param nums The input array to minimize
9     * @return The minimum possible maximum value after operations
10     */
11    public int minimizeArrayValue(int[] nums) {
12        this.nums = nums;
13      
14        // Binary search range: [0, maximum element in array]
15        int left = 0;
16        int right = findMaxElement(nums);
17      
18        // Binary search for the minimum possible maximum value
19        while (left < right) {
20            int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
21          
22            if (canAchieveMaxValue(mid)) {
23                // If we can achieve this max value, try to find a smaller one
24                right = mid;
25            } else {
26                // If we cannot achieve this max value, we need a larger one
27                left = mid + 1;
28            }
29        }
30      
31        return left;
32    }
33
34    /**
35     * Checks if it's possible to make all elements <= targetMax.
36     * Works backwards from the end of array, accumulating excess values.
37     * 
38     * @param targetMax The target maximum value to check
39     * @return true if achievable, false otherwise
40     */
41    private boolean canAchieveMaxValue(int targetMax) {
42        long excessValue = 0;
43      
44        // Process array from right to left (excluding first element)
45        for (int i = nums.length - 1; i > 0; i--) {
46            // Calculate excess: if current element + accumulated excess > targetMax,
47            // we need to move the excess to the left
48            excessValue = Math.max(0, excessValue + nums[i] - targetMax);
49        }
50      
51        // Check if first element plus all accumulated excess <= targetMax
52        return nums[0] + excessValue <= targetMax;
53    }
54
55    /**
56     * Finds the maximum element in the array.
57     * 
58     * @param nums The input array
59     * @return The maximum element value
60     */
61    private int findMaxElement(int[] nums) {
62        int maxValue = nums[0];
63      
64        for (int element : nums) {
65            maxValue = Math.max(maxValue, element);
66        }
67      
68        return maxValue;
69    }
70}
71
1class Solution {
2public:
3    int minimizeArrayValue(vector<int>& nums) {
4        // Binary search on the answer: find the minimum possible maximum value
5        int minPossibleMax = 0;
6        int maxPossibleMax = *max_element(nums.begin(), nums.end());
7      
8        // Lambda function to check if we can achieve a maximum value of 'targetMax'
9        // Strategy: Process from right to left, pushing excess values to the left
10        auto canAchieveMaximum = [&](int targetMax) -> bool {
11            long long excessToDistribute = 0;
12          
13            // Iterate from the last element to the second element (index 1)
14            for (int i = nums.size() - 1; i > 0; --i) {
15                // Calculate how much the current element exceeds targetMax
16                // Add any excess from previous elements that needs to be distributed
17                excessToDistribute = max(0LL, excessToDistribute + nums[i] - targetMax);
18            }
19          
20            // Check if the first element plus all accumulated excess doesn't exceed targetMax
21            return nums[0] + excessToDistribute <= targetMax;
22        };
23      
24        // Binary search to find the minimum achievable maximum value
25        while (minPossibleMax < maxPossibleMax) {
26            int midValue = minPossibleMax + (maxPossibleMax - minPossibleMax) / 2;
27          
28            if (canAchieveMaximum(midValue)) {
29                // If we can achieve midValue, try to find a smaller maximum
30                maxPossibleMax = midValue;
31            } else {
32                // If we cannot achieve midValue, we need a larger maximum
33                minPossibleMax = midValue + 1;
34            }
35        }
36      
37        return minPossibleMax;
38    }
39};
40
1function minimizeArrayValue(nums: number[]): number {
2    // Binary search on the answer: find the minimum possible maximum value
3    let minPossibleMax: number = 0;
4    let maxPossibleMax: number = Math.max(...nums);
5  
6    // Function to check if we can achieve a maximum value of 'targetMax'
7    // Strategy: Process from right to left, pushing excess values to the left
8    const canAchieveMaximum = (targetMax: number): boolean => {
9        let excessToDistribute: number = 0;
10      
11        // Iterate from the last element to the second element (index 1)
12        // We can only move values from right to left (decrement operation)
13        for (let i = nums.length - 1; i > 0; i--) {
14            // Calculate how much the current element exceeds targetMax
15            // Add any excess from previous elements that needs to be distributed
16            excessToDistribute = Math.max(0, excessToDistribute + nums[i] - targetMax);
17        }
18      
19        // Check if the first element plus all accumulated excess doesn't exceed targetMax
20        // The first element must absorb all excess since we can't move values further left
21        return nums[0] + excessToDistribute <= targetMax;
22    };
23  
24    // Binary search to find the minimum achievable maximum value
25    while (minPossibleMax < maxPossibleMax) {
26        // Calculate middle value avoiding overflow
27        const midValue: number = minPossibleMax + Math.floor((maxPossibleMax - minPossibleMax) / 2);
28      
29        if (canAchieveMaximum(midValue)) {
30            // If we can achieve midValue as maximum, try to find a smaller maximum
31            maxPossibleMax = midValue;
32        } else {
33            // If we cannot achieve midValue as maximum, we need a larger maximum
34            minPossibleMax = midValue + 1;
35        }
36    }
37  
38    // Return the minimum possible maximum value after all operations
39    return minPossibleMax;
40}
41

Time and Space Complexity

Time Complexity: O(n × log M)

The algorithm uses binary search on the answer space from 0 to max(nums) (which is M). The binary search performs O(log M) iterations. In each iteration, the check function is called, which iterates through the array from the second-to-last element to the first element (excluding index 0), taking O(n) time where n is the length of the array. Therefore, the overall time complexity is O(n × log M).

Space Complexity: O(1)

The algorithm only uses a constant amount of extra space. The variables left, right, mid, and d in the check function all use constant space. The iteration through the array in the check function doesn't create any additional data structures proportional to the input size. Therefore, the space complexity is O(1).

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

Common Pitfalls

1. Incorrect Iteration Direction in Check Function

A common mistake is iterating from left to right instead of right to left when checking feasibility. This leads to incorrect logic because:

Wrong Approach:

def check(mx):
    deficit = 0
    for i in range(len(nums)):  # LEFT to RIGHT - WRONG!
        if nums[i] + deficit > mx:
            deficit = nums[i] + deficit - mx
    return True  # No way to validate this

Why it fails: When going left to right, you can't determine what excess will arrive from the right, making it impossible to correctly calculate redistributions.

Correct Approach:

def check(mx):
    deficit = 0
    for x in nums[:0:-1]:  # RIGHT to LEFT - CORRECT!
        deficit = max(0, deficit + x - mx)
    return nums[0] + deficit <= mx

2. Forgetting to Handle the First Element Separately

The first element (index 0) is special - it cannot pass values to the left. A common error is treating it like other elements:

Wrong:

def check(mx):
    deficit = 0
    for x in reversed(nums):  # Includes nums[0] in the loop
        deficit = max(0, deficit + x - mx)
    return deficit == 0  # Wrong validation

Correct:

def check(mx):
    deficit = 0
    for x in nums[:0:-1]:  # Excludes nums[0]
        deficit = max(0, deficit + x - mx)
    return nums[0] + deficit <= mx  # Separate check for nums[0]

3. Integer Overflow with Large Values

When working with large numbers, accumulated deficits can overflow. While Python handles big integers automatically, in other languages this requires attention:

Potential Issue (in languages like Java/C++):

# If nums contains very large values, deficit accumulation might overflow
deficit = deficit + x - mx  # Could overflow in other languages

Solution: Use appropriate data types (long long in C++, long in Java) or add overflow checks.

4. Off-by-One Error in Binary Search

Using the wrong binary search template can cause infinite loops or miss the answer:

Wrong:

while left <= right:  # Using <= can cause issues
    mid = (left + right) // 2
    if check(mid):
        right = mid - 1  # Might skip the answer
    else:
        left = mid + 1

Correct:

while left < right:  # Use < for this template
    mid = (left + right) // 2
    if check(mid):
        right = mid  # Keep mid as potential answer
    else:
        left = mid + 1

5. Misunderstanding the Operation Direction

Some might think values can move in both directions:

Wrong Mental Model: "I can redistribute values freely between adjacent elements"

Correct Understanding: "Values can ONLY move from right to left (from nums[i] to nums[i-1])"

This fundamentally changes the problem - if bidirectional movement were allowed, you could simply average all values, but the constraint makes it more complex.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More