Minimum Pair Removal to Sort Array II
HardPrompt
Given an array nums, you can perform the following operation any number of times:
- Select the adjacent pair with the minimum sum in
nums. If multiple such pairs exist, choose the leftmost one. - Replace the pair with their sum.
Return the minimum number of operations needed to make the array non-decreasing.
An array is said to be non-decreasing if each element is greater than or equal to its previous element (if it exists).
Example 1:
Input: nums = [5,2,3,1]
Output: 2
Explanation:
- The pair
(3,1)has the minimum sum of 4. After replacement,nums = [5,2,4]. - The pair
(2,4)has the minimum sum of 6. After replacement,nums = [5,6].
The array nums became non-decreasing in two operations.
Example 2:
Input: nums = [1,2,2]
Output: 0
Explanation:
The array nums is already sorted.
Constraints:
1 <= nums.length <= 105-109 <= nums[i] <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. It uses a dynamic list, such as java.util.ArrayList, to represent the array nums. In each step of the simulation, it performs a linear scan through the list to find the adjacent pair with the minimum sum. After finding this pair, it merges them into a single element and increments the operation count. This process is repeated until the entire list becomes non-decreasing.
Algorithm
-
- Initialize
operations = 0and convert the input arraynumsinto ajava.util.List<Long>to handle large numbers and potential overflows.
- Initialize
-
- Start an infinite loop that will be broken once the list is sorted.
-
- Inside the loop, first check if the current list is sorted in non-decreasing order. If it is, break the loop.
-
- If not sorted, iterate through all adjacent pairs in the list to find the one with the minimum sum. Keep track of this minimum sum and the index of the first element of the pair (
minIndex).
- If not sorted, iterate through all adjacent pairs in the list to find the one with the minimum sum. Keep track of this minimum sum and the index of the first element of the pair (
-
- Perform the merge operation: calculate the sum of the elements at
minIndexandminIndex + 1. Update the element atminIndexwith this sum and remove the element atminIndex + 1.
- Perform the merge operation: calculate the sum of the elements at
-
- Increment the
operationscounter.
- Increment the
-
- Once the loop terminates, return the total
operationscount.
- Once the loop terminates, return the total
Walkthrough
The brute-force simulation is the most straightforward way to solve the problem. We use a List<Long> to store the numbers, which allows for dynamic resizing and prevents integer overflow when summing up large numbers.
The simulation proceeds in a loop. In each iteration, we first need to determine if we should stop. This is done by a helper function isSorted, which traverses the list to check if list[i] >= list[i-1] for all i. If the list is sorted, we exit the loop.
If the list is not yet sorted, we proceed with one operation. We iterate through all adjacent pairs from the beginning of the list, calculating their sum and keeping track of the minimum sum found so far, along with the index of that pair. The problem specifies that if there's a tie in sums, the leftmost pair should be chosen, which is naturally handled by a simple linear scan from left to right.
After finding the pair (list[minIndex], list[minIndex+1]) with the minimum sum, we replace them with their sum. In an ArrayList, this involves updating the value at minIndex and removing the element at minIndex+1. Each such merge constitutes one operation, so we increment our counter.
This cycle of checking, finding the minimum, and merging continues until the list is non-decreasing.
import java.util.ArrayList;import java.util.List; class Solution { public int minOperations(int[] nums) { if (nums.length <= 1) { return 0; } List<Long> currentNums = new ArrayList<>(); for (int num : nums) { currentNums.add((long) num); } int operations = 0; while (true) { if (isSorted(currentNums)) { break; } long minSum = Long.MAX_VALUE; int minIndex = -1; for (int i = 0; i < currentNums.size() - 1; i++) { long currentSum = currentNums.get(i) + currentNums.get(i + 1); if (currentSum < minSum) { minSum = currentSum; minIndex = i; } } long val1 = currentNums.get(minIndex); long val2 = currentNums.get(minIndex + 1); currentNums.set(minIndex, val1 + val2); currentNums.remove(minIndex + 1); operations++; } return operations; } private boolean isSorted(List<Long> nums) { for (int i = 1; i < nums.size(); i++) { if (nums.get(i) < nums.get(i - 1)) { return false; } } return true; }}Complexity
Time
O(K * N), where N is the initial length of the array and K is the number of operations. Each operation involves scanning the list to check for sortedness (O(N)), finding the minimum sum pair (O(N)), and updating the list (O(N) for an ArrayList). Since K can be up to N-1, the worst-case time complexity is O(N^2).
Space
O(N), where N is the number of elements in the input array. This space is used to store the list of numbers.
Trade-offs
Pros
Simple to understand and implement.
Directly follows the logic described in the problem statement.
Cons
Highly inefficient for large inputs due to its quadratic time complexity.
Will likely result in a 'Time Limit Exceeded' (TLE) error on competitive programming platforms for the given constraints.
This approach optimizes the simulation by using more efficient data structures to handle the main operations. A doubly linked list is used to represent the array, which allows for O(1) element removal and neighbor access. A min-priority queue is employed to find the minimum sum pair efficiently in O(log N) time. The termination condition (checking if the array is sorted) is also optimized by maintaining a real-time count of inversions (adjacent elements a, b where a > b). The simulation stops when this count drops to zero.
Algorithm
-
- Initialization:
- Create a Doubly Linked List (DLL) from
nums, where each node stores its value and a unique ID (its original index). Add sentinel head and tail nodes. - Create a Min-Priority Queue to store
Pairobjects. APaircontains asumand a reference to the firstNodeof the pair. - Populate the PQ with all initial adjacent pairs from the DLL.
- Compute the initial number of
inversions(wherenode.val > node.next.val).
-
- Simulation Loop: Continue as long as
inversions > 0.
- a. Extract the
Pairwith the minimum sum from the PQ. - b. Staleness Check: Verify the extracted pair is still valid. A pair is stale if its nodes have been removed or if their values have changed such that their current sum doesn't match the sum stored in the
Pair. If stale, discard and continue to the next pair in the PQ. - c. Merge: If the pair
(u, v)is valid:- Increment
operationscount. - Update the
inversionscount: decrement for inversions removed by the merge, and increment for any new inversions created. - Update the DLL: set
u.val = u.val + v.val, and bypassvby settingu.next = v.next. Markvas removed. - Update the PQ: Add new pairs formed by the updated node
uand its new neighbors (u.prev,u.next).
- Increment
- Simulation Loop: Continue as long as
-
- Termination: When the loop finishes (
inversionsis 0), return the totaloperations.
- Termination: When the loop finishes (
Walkthrough
To overcome the O(N^2) complexity of the naive simulation, we need to speed up two key steps: finding the minimum sum pair and checking for the sorted condition.
-
Finding Minimum Sum Pair: A min-priority queue (min-heap) is perfect for this. We store objects representing each adjacent pair's sum and a reference to the pair's first node. This lets us retrieve the minimum sum pair in O(log N) time.
-
Efficient Merging: An
ArrayListis slow for removals in the middle. A doubly linked list (DLL) is ideal here. Each number is a node in the DLL. Merging a pair(u, v)involves updatingu.val, changingu.nextto point tov.next, and updating the back-pointer ofv.next. This is an O(1) operation. -
Checking Sorted Condition: Linearly scanning the list in each step is too slow. Instead, we can maintain a counter for the number of
inversions(places wherenode.val > node.next.val). We initialize this count once. After each merge, we only need to check the newly formed adjacent pairs to update the inversion count in O(1) time. The simulation runs untilinversionsbecomes 0. -
Handling Stale Data: When we merge a pair, the pairs involving its neighbors become invalid. Instead of trying to find and remove these from the priority queue (which is inefficient), we simply add the new pairs. When we extract a pair from the PQ, we perform a staleness check. A pair is stale if one of its nodes has been removed or if the sum of the current values of its nodes doesn't match the sum stored in the PQ. We discard stale pairs and process the next one.
import java.util.PriorityQueue; class Solution { static class Node { long val; Node prev, next; int id; // Unique identifier for the node, e.g., original index Node(long val, int id) { this.val = val; this.id = id; } } static class Pair implements Comparable<Pair> { long sum; Node node; // The first node of the pair Pair(long sum, Node node) { this.sum = sum; this.node = node; } @Override public int compareTo(Pair other) { if (this.sum != other.sum) { return Long.compare(this.sum, other.sum); } // Tie-breaking: choose the leftmost pair return Integer.compare(this.node.id, other.node.id); } } public int minOperations(int[] nums) { if (nums.length <= 1) return 0; Node head = new Node(0, -1); Node tail = new Node(0, nums.length); Node curr = head; int inversions = 0; Node[] nodes = new Node[nums.length]; for (int i = 0; i < nums.length; i++) { nodes[i] = new Node(nums[i], i); curr.next = nodes[i]; nodes[i].prev = curr; curr = nodes[i]; if (i > 0 && nodes[i-1].val > nodes[i].val) { inversions++; } } curr.next = tail; tail.prev = curr; PriorityQueue<Pair> pq = new PriorityQueue<>(); for (int i = 0; i < nums.length - 1; i++) { pq.add(new Pair(nodes[i].val + nodes[i+1].val, nodes[i])); } int operations = 0; boolean[] removed = new boolean[nums.length]; while (inversions > 0 && !pq.isEmpty()) { Pair p = pq.poll(); Node u = p.node; if (removed[u.id] || u.next == tail) continue; Node v = u.next; if (removed[v.id]) continue; // Stale pair check if (u.val + v.val != p.sum) continue; operations++; // Update inversions before merge if (u.prev != head && u.prev.val > u.val) inversions--; if (u.val > v.val) inversions--; if (v.next != tail && v.val > v.next.val) inversions--; // Perform merge u.val += v.val; removed[v.id] = true; u.next = v.next; v.next.prev = u; // Update inversions after merge if (u.prev != head && u.prev.val > u.val) inversions++; if (u.next != tail && u.val > u.next.val) inversions++; // Add new pairs to PQ if (u.prev != head) { pq.add(new Pair(u.prev.val + u.val, u.prev)); } if (u.next != tail) { pq.add(new Pair(u.val + u.next.val, u)); } } return operations; }}Complexity
Time
O(K * log N), where N is the initial length of the array and K is the number of operations. The main loop runs K times. Each iteration involves at least one O(log N) poll from the priority queue and potentially two O(log N) additions. The number of stale entries polled does not change the overall asymptotic complexity. In the worst case, K is O(N), leading to a total time complexity of O(N log N).
Space
O(N), where N is the initial number of elements. This space is required for the doubly linked list (O(N)), the priority queue (can contain O(N) pairs), and the auxiliary `removed` array (O(N)).
Trade-offs
Pros
Highly efficient, with a time complexity suitable for large inputs.
Optimizes the key bottlenecks of the simulation by using appropriate data structures.
The inversion counting method avoids costly checks for sortedness in each iteration.
Cons
Significantly more complex to implement than the brute-force approach.
Requires careful management of data structures, pointers, and state (especially for staleness checks and inversion counting).
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.