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 element9
to make the array non-decreasing - You could split
9
into smaller parts that fit between or equal to3
and3
- 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.
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 updatemx = 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:
- Minimize the number of pieces (fewer pieces = fewer operations)
- 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.
Solution Approach
Let's implement the greedy approach by traversing the array from right to left:
-
Initialize variables:
ans = 0
to count the total operationsn = len(nums)
for array lengthmx = nums[-1]
as the maximum allowed value (starting with the last element)
-
Traverse backwards from index
n-2
to0
: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
(sincek
pieces requirek-1
splits) - Update
mx
to the smallest piece value:mx = nums[i] // k
-
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
- Need
- 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 EvaluatorExample 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 requiresk-1
operations (cuts) - Adding
k
instead ofk-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 whenn=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
How does quick sort divide the problem into subproblems?
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
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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!