Facebook Pixel

2366. Minimum Replacements to Sort the Array

Problem Description

You have an integer array nums indexed from 0. You can perform an operation where you replace any element in the array with two elements that add up to the original element's value.

For instance, if you have nums = [5,6,7], you could replace the element 6 (at index 1) with 2 and 4, transforming the array into [5,2,4,7].

Your goal is to find the minimum number of operations needed to make the array sorted in non-decreasing order (each element is less than or equal to the next element).

The key insight is that when you replace an element with two smaller elements, you're essentially splitting it. You need to strategically decide which elements to split and how to split them so that the final array becomes sorted with the fewest operations possible.

For example:

  • If nums = [3,9,3], you need to split the middle element 9 to make the array non-decreasing
  • You could split 9 into smaller parts that fit between or equal to 3 and 3
  • The minimum operations would be determined by how you optimally split such elements

The challenge is to determine which elements must be split and the optimal way to split them to achieve a sorted array with minimal operations.

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

Intuition

Let's think about this problem backwards. If we process from left to right, we don't know what values we'll encounter later, making it hard to decide how to split current elements optimally. But if we process from right to left, we can make better decisions.

The key observation is that the last element of the final sorted array should be as large as possible. Why? Because a larger last element gives us more room to work with for the elements before it. Since we want a non-decreasing array, each element must be the next element. Therefore, there's no benefit in splitting the last element - we should keep it as large as possible.

Working backwards from index n-1 to 0, we maintain a variable mx representing the maximum value that the current position can have (initially mx = nums[n-1]).

For each element nums[i]:

  • If nums[i] ≤ mx, perfect! We don't need to split it. We update mx = nums[i] since this becomes the new upper bound for elements to its left.
  • If nums[i] > mx, we must split it into smaller pieces, each ≤ mx.

When we need to split nums[i] into pieces each ≤ mx, what's the optimal strategy? We want to:

  1. Minimize the number of pieces (fewer pieces = fewer operations)
  2. Make the smallest piece as large as possible (to give more room for elements to the left)

The minimum number of pieces needed is k = ⌈nums[i] / mx⌉. This requires k-1 operations (splitting once creates 2 pieces, splitting twice creates 3 pieces, etc.).

To make the pieces as equal as possible (maximizing the minimum piece), we divide nums[i] into k roughly equal parts. The smallest piece will be ⌊nums[i] / k⌋, which becomes our new mx for the next iteration.

This greedy approach ensures we perform the minimum operations while maintaining the maximum possible values at each position, giving us the optimal solution.

Learn more about Greedy and Math patterns.

Solution Approach

Let's implement the greedy approach by traversing the array from right to left:

  1. Initialize variables:

    • ans = 0 to count the total operations
    • n = len(nums) for array length
    • mx = nums[-1] as the maximum allowed value (starting with the last element)
  2. Traverse backwards from index n-2 to 0:

    For each element nums[i]:

    Case 1: No split needed (nums[i] ≤ mx)

    • The element fits within our constraint
    • Update mx = nums[i] as the new upper bound for elements to the left
    • Continue to the next element

    Case 2: Split required (nums[i] > mx)

    • Calculate minimum pieces needed: k = ⌈nums[i] / mx⌉
    • We can compute this using: k = (nums[i] + mx - 1) // mx
    • Add operations to answer: ans += k - 1 (since k pieces require k-1 splits)
    • Update mx to the smallest piece value: mx = nums[i] // k
  3. Return the total operations

Example walkthrough:

Consider nums = [3, 9, 3]:

  • Start with mx = 3 (last element)
  • At index 1: nums[1] = 9 > mx = 3
    • Need k = ⌈9/3⌉ = 3 pieces
    • Operations: 3 - 1 = 2
    • New mx = ⌊9/3⌋ = 3
  • At index 0: nums[0] = 3 ≤ mx = 3
    • No split needed
  • Total operations: 2

Time Complexity: O(n) - single pass through the array Space Complexity: O(1) - only using a few variables

The key insight is that by processing right-to-left and greedily minimizing splits while maximizing the smallest piece value, we ensure the optimal solution.

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 = [4, 7, 2]:

Initial Setup:

  • ans = 0 (operation counter)
  • mx = 2 (last element, which sets our initial maximum)
  • We'll traverse from right to left: index 1 → index 0

Step 1: Process index 1 (value = 7)

  • Current element: nums[1] = 7
  • Check: Is 7 ≤ mx (2)? No, 7 > 2, so we need to split
  • Calculate pieces needed: k = ⌈7/2⌉ = 4 pieces
  • Operations needed: k - 1 = 4 - 1 = 3 operations
  • Update ans = 0 + 3 = 3
  • New mx = ⌊7/4⌋ = 1 (smallest piece when dividing 7 into 4 parts)
  • Array conceptually becomes: [4, 1, 1, 2, 2, 2] (though we don't actually modify it)

Step 2: Process index 0 (value = 4)

  • Current element: nums[0] = 4
  • Check: Is 4 ≤ mx (1)? No, 4 > 1, so we need to split
  • Calculate pieces needed: k = ⌈4/1⌉ = 4 pieces
  • Operations needed: k - 1 = 4 - 1 = 3 operations
  • Update ans = 3 + 3 = 6
  • New mx = ⌊4/4⌋ = 1 (smallest piece when dividing 4 into 4 parts)
  • Array conceptually becomes: [1, 1, 1, 1, 1, 1, 2, 2, 2]

Result: Total operations = 6

The final conceptual array [1, 1, 1, 1, 1, 1, 2, 2, 2] is sorted in non-decreasing order, achieved with 6 operations (3 operations to split the 7, and 3 operations to split the 4).

Solution Implementation

1class Solution:
2    def minimumReplacement(self, nums: List[int]) -> int:
3        """
4        Minimize the number of operations to make the array non-increasing.
5        Each operation replaces an element with two elements that sum to the original.
6      
7        Strategy: Process from right to left, ensuring each element is <= the next element.
8        """
9        total_operations = 0
10        array_length = len(nums)
11      
12        # Start with the rightmost element as the maximum allowed value
13        max_allowed_value = nums[-1]
14      
15        # Process elements from right to left (second-to-last to first)
16        for index in range(array_length - 2, -1, -1):
17            current_value = nums[index]
18          
19            # If current element is already <= max_allowed_value, no operations needed
20            if current_value <= max_allowed_value:
21                max_allowed_value = current_value
22                continue
23          
24            # Calculate minimum number of parts to split current element
25            # Using ceiling division: (current_value + max_allowed_value - 1) // max_allowed_value
26            num_parts = (current_value + max_allowed_value - 1) // max_allowed_value
27          
28            # Number of operations = number of parts - 1 (splitting into k parts takes k-1 operations)
29            total_operations += num_parts - 1
30          
31            # Update max_allowed_value to the smallest value after optimal splitting
32            # This ensures the leftmost split value is minimized for future iterations
33            max_allowed_value = current_value // num_parts
34      
35        return total_operations
36
1class Solution {
2    public long minimumReplacement(int[] nums) {
3        long totalOperations = 0;
4        int n = nums.length;
5      
6        // Start with the last element as the maximum allowed value
7        // (since we want non-decreasing order from right to left)
8        int maxAllowed = nums[n - 1];
9      
10        // Traverse the array from right to left
11        for (int i = n - 2; i >= 0; i--) {
12            // If current element is already less than or equal to maxAllowed,
13            // no replacement needed, update maxAllowed
14            if (nums[i] <= maxAllowed) {
15                maxAllowed = nums[i];
16                continue;
17            }
18          
19            // Calculate minimum number of parts needed to split nums[i]
20            // such that each part is at most maxAllowed
21            // Using ceiling division: (nums[i] + maxAllowed - 1) / maxAllowed
22            int numberOfParts = (nums[i] + maxAllowed - 1) / maxAllowed;
23          
24            // Number of operations = number of parts - 1
25            // (splitting into k parts requires k-1 operations)
26            totalOperations += numberOfParts - 1;
27          
28            // Update maxAllowed to be the minimum possible value
29            // when nums[i] is split evenly into numberOfParts
30            // This ensures the leftmost part is as large as possible
31            maxAllowed = nums[i] / numberOfParts;
32        }
33      
34        return totalOperations;
35    }
36}
37
1class Solution {
2public:
3    long long minimumReplacement(vector<int>& nums) {
4        long long totalOperations = 0;
5        int n = nums.size();
6      
7        // Start with the last element as the maximum allowed value
8        // (no element to its right can be violated)
9        int maxAllowed = nums[n - 1];
10      
11        // Traverse the array from right to left
12        for (int i = n - 2; i >= 0; --i) {
13            // If current element is already <= maxAllowed, no replacement needed
14            if (nums[i] <= maxAllowed) {
15                // Update maxAllowed to current element (maintaining non-increasing property)
16                maxAllowed = nums[i];
17                continue;
18            }
19          
20            // Current element is larger than maxAllowed, need to split it
21            // Calculate minimum number of parts needed to ensure each part <= maxAllowed
22            // Using ceiling division: (nums[i] + maxAllowed - 1) / maxAllowed
23            int numParts = (nums[i] + maxAllowed - 1) / maxAllowed;
24          
25            // Number of operations = number of parts - 1
26            totalOperations += numParts - 1;
27          
28            // To minimize operations for elements to the left,
29            // maximize the smallest element after splitting
30            // The smallest element will be nums[i] / numParts (floor division)
31            maxAllowed = nums[i] / numParts;
32        }
33      
34        return totalOperations;
35    }
36};
37
1/**
2 * Finds the minimum number of replacement operations needed to make the array non-increasing.
3 * Each replacement operation splits a number into two positive integers that sum to the original.
4 * @param nums - The input array of positive integers
5 * @returns The minimum number of replacement operations needed
6 */
7function minimumReplacement(nums: number[]): number {
8    const arrayLength: number = nums.length;
9  
10    // Start with the last element as the maximum allowed value
11    let maxAllowedValue: number = nums[arrayLength - 1];
12  
13    // Counter for the total number of replacement operations
14    let totalReplacements: number = 0;
15  
16    // Traverse the array from right to left (second last to first element)
17    for (let currentIndex: number = arrayLength - 2; currentIndex >= 0; currentIndex--) {
18        // If current element is already less than or equal to max allowed, no replacement needed
19        if (nums[currentIndex] <= maxAllowedValue) {
20            // Update max allowed value for next iteration
21            maxAllowedValue = nums[currentIndex];
22            continue;
23        }
24      
25        // Calculate minimum number of parts needed to split current element
26        // such that each part is at most maxAllowedValue
27        const numberOfParts: number = Math.ceil(nums[currentIndex] / maxAllowedValue);
28      
29        // Number of replacements is one less than number of parts
30        // (splitting into k parts requires k-1 operations)
31        totalReplacements += numberOfParts - 1;
32      
33        // Update max allowed value to be the smallest possible value after splitting
34        // This ensures the array remains non-increasing
35        maxAllowedValue = Math.floor(nums[currentIndex] / numberOfParts);
36    }
37  
38    return totalReplacements;
39}
40

Time and Space Complexity

The time complexity is O(n), where n is the length of the array nums. The algorithm iterates through the array exactly once from right to left (from index n-2 to 0), performing constant-time operations at each index. Even though there are arithmetic operations like division and modulo inside the loop, these operations take O(1) time for integer arithmetic.

The space complexity is O(1). The algorithm only uses a fixed amount of extra space regardless of the input size. The variables used are ans (to store the result), n (to store the array length), mx (to track the maximum allowed value), i (loop counter), and k (temporary variable for calculations). No additional data structures that scale with input size are created.

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

Common Pitfalls

1. Incorrect Ceiling Division Formula

Pitfall: Using standard ceiling division with floating-point operations like math.ceil(current_value / max_allowed_value) can lead to floating-point precision errors, especially with large integers.

Why it's problematic:

  • Floating-point arithmetic can introduce rounding errors
  • For large integers, precision loss might cause incorrect results
  • Extra import of math module is unnecessary

Solution: Use integer-only ceiling division formula:

# Correct: Integer ceiling division
num_parts = (current_value + max_allowed_value - 1) // max_allowed_value

# Avoid: Floating-point ceiling
num_parts = math.ceil(current_value / max_allowed_value)

2. Wrong Update of Maximum Allowed Value After Splitting

Pitfall: After splitting an element, incorrectly updating the max_allowed_value can break the algorithm. A common mistake is using the remainder or the ceiling division result instead of floor division.

Why it's problematic:

  • Using current_value % num_parts gives the remainder, not the minimum piece value
  • Using (current_value + num_parts - 1) // num_parts gives the maximum piece value, not minimum
  • This leads to suboptimal splits in subsequent iterations

Solution: Always use floor division to get the minimum piece value:

# Correct: Minimum piece value ensures optimal future splits
max_allowed_value = current_value // num_parts

# Wrong: Using remainder
max_allowed_value = current_value % num_parts

# Wrong: Using ceiling (maximum piece value)
max_allowed_value = (current_value + num_parts - 1) // num_parts

3. Processing Array in Wrong Direction

Pitfall: Processing the array from left to right instead of right to left.

Why it's problematic:

  • When processing left to right, you don't know what constraints future elements will impose
  • This can lead to unnecessary splits or inability to determine optimal split values
  • The problem becomes significantly more complex or unsolvable with a greedy approach

Solution: Always process from right to left:

# Correct: Right to left traversal
for index in range(len(nums) - 2, -1, -1):
    # Process nums[index]

# Wrong: Left to right traversal
for index in range(len(nums) - 1):
    # This won't work with the greedy approach

4. Off-by-One Error in Operations Count

Pitfall: Confusing the number of pieces with the number of operations needed.

Why it's problematic:

  • Splitting into k pieces requires k-1 operations (cuts)
  • Adding k instead of k-1 to the operation count gives wrong answer

Solution: Remember that operations = pieces - 1:

# Correct: k pieces need k-1 operations
total_operations += num_parts - 1

# Wrong: Adding number of pieces directly
total_operations += num_parts

5. Edge Case: Single Element Array

Pitfall: Not handling arrays with only one element properly.

Why it's problematic:

  • The loop range(n-2, -1, -1) becomes empty when n=1
  • While this naturally returns 0 (correct answer), it's important to understand why

Solution: The current implementation handles this correctly, but be aware:

# For nums = [5], the loop doesn't execute, returning 0 operations (correct)
# But explicitly handling might make code clearer:
if len(nums) <= 1:
    return 0
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

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

Load More