|
| 1 | +# [Problem 3510: Minimum Pair Removal to Sort Array II](https://leetcode.com/problems/minimum-pair-removal-to-sort-array-ii/description/?envType=daily-question) |
| 2 | + |
| 3 | +## Initial thoughts (stream-of-consciousness) |
| 4 | +This operation is deterministic at each step: you must merge the adjacent pair with the minimum sum (leftmost in ties). Repeating merges collapses the array into fewer elements; we can stop once the current array is non-decreasing. So we need to simulate the forced sequence of merges (but stop early when the array becomes non-decreasing) and return how many merges were required. |
| 5 | + |
| 6 | +Naively simulating by rebuilding the array each time would be O(n^2) in the worst case. A standard technique for merging adjacent elements with a global "min adjacent pair" policy is to keep the adjacent-pair sums in a min-heap and update local neighbors when we actually perform a merge (like maintaining a linked list of elements and a heap of adjacent-pair sums). Also, to quickly know when the current list is non-decreasing we can maintain the number of "bad" adjacent pairs (where left > right) and update that count only for pairs affected by a merge. |
| 7 | + |
| 8 | +So approach: doubly-linked nodes for elements, a heap of (sum, left_index_tie, left_node_id/ptr) to pick the leftmost minimal-sum adjacent pair; when merging two nodes, update prev/next pointers and update the bad-pair count only for affected adjacent pairs. Stop once bad_count == 0. |
| 9 | + |
| 10 | +## Refining the problem, round 2 thoughts |
| 11 | +Edge cases: |
| 12 | +- Already non-decreasing array -> return 0 immediately. |
| 13 | +- Single element -> 0. |
| 14 | +- Negative numbers allowed (sums may be negative) — heap supports them. |
| 15 | +- Need to ensure we skip stale heap entries (pairs where one of the nodes has been removed or the adjacency changed). Use an "alive" flag in nodes and verify left.next is still the original right node. |
| 16 | + |
| 17 | +Complexity: |
| 18 | +- Each merge reduces the number of nodes by 1; at most n-1 merges. |
| 19 | +- For each merge we perform O(log n) heap operations (pop and up to two pushes). |
| 20 | +- Updating bad-pair count is O(1) per merge. |
| 21 | +Overall O(n log n) time and O(n) extra space. |
| 22 | + |
| 23 | +Implementation details: |
| 24 | +- Node holds value, original leftmost index (for tie-break), prev, next, alive flag. |
| 25 | +- Heap entries: (sum, left_index, id(left), left_node). We include id(left) to detect stale entries if necessary. |
| 26 | +- When popping from heap, validate that left.alive and left.next exists and is alive and that id(left) matches. |
| 27 | +- When merging (left, right) -> new node with value left.val + right.val and index = left.index (leftmost), splice into the linked list, mark left/right dead, update heap for new adjacencies, and update bad_count by subtracting old bad statuses and adding new ones for pairs (left_prev,left), (left,right), (right,right_next) -> replaced by (left_prev, new), (new, right_next). |
| 28 | + |
| 29 | +## Attempted solution(s) |
| 30 | +```python |
| 31 | +import heapq |
| 32 | + |
| 33 | +class Node: |
| 34 | + __slots__ = ("val", "idx", "prev", "next", "alive") |
| 35 | + def __init__(self, val, idx): |
| 36 | + self.val = val |
| 37 | + self.idx = idx # tie-break: leftmost original position |
| 38 | + self.prev = None |
| 39 | + self.next = None |
| 40 | + self.alive = True |
| 41 | + |
| 42 | +class Solution: |
| 43 | + def minimumOperations(self, nums: list[int]) -> int: |
| 44 | + n = len(nums) |
| 45 | + if n <= 1: |
| 46 | + return 0 |
| 47 | + |
| 48 | + # Build doubly linked list of nodes |
| 49 | + nodes = [Node(nums[i], i) for i in range(n)] |
| 50 | + for i in range(n - 1): |
| 51 | + nodes[i].next = nodes[i + 1] |
| 52 | + nodes[i + 1].prev = nodes[i] |
| 53 | + |
| 54 | + # initial bad (descending) adjacent pair count |
| 55 | + bad = 0 |
| 56 | + for i in range(n - 1): |
| 57 | + if nodes[i].val > nodes[i + 1].val: |
| 58 | + bad += 1 |
| 59 | + if bad == 0: |
| 60 | + return 0 |
| 61 | + |
| 62 | + # heap of (sum, left_idx, left_id, left_node) |
| 63 | + heap = [] |
| 64 | + for i in range(n - 1): |
| 65 | + left = nodes[i] |
| 66 | + right = nodes[i + 1] |
| 67 | + heapq.heappush(heap, (left.val + right.val, left.idx, id(left), left)) |
| 68 | + |
| 69 | + ops = 0 |
| 70 | + while heap: |
| 71 | + s, lidx, lid, left = heapq.heappop(heap) |
| 72 | + # validate still a valid adjacent pair |
| 73 | + if not left.alive: |
| 74 | + continue |
| 75 | + right = left.next |
| 76 | + if right is None or (not right.alive): |
| 77 | + continue |
| 78 | + if id(left) != lid: |
| 79 | + # stale entry (left has been replaced) |
| 80 | + continue |
| 81 | + |
| 82 | + # We will merge left and right |
| 83 | + left_prev = left.prev |
| 84 | + right_next = right.next |
| 85 | + |
| 86 | + # subtract contributions of old adjacent pairs to bad |
| 87 | + if left_prev is not None and left_prev.alive: |
| 88 | + if left_prev.val > left.val: |
| 89 | + bad -= 1 |
| 90 | + if left.val > right.val: |
| 91 | + bad -= 1 |
| 92 | + if right_next is not None and right_next.alive: |
| 93 | + if right.val > right_next.val: |
| 94 | + bad -= 1 |
| 95 | + |
| 96 | + # create new node as merge of left and right |
| 97 | + new_val = left.val + right.val |
| 98 | + new_node = Node(new_val, left.idx) |
| 99 | + # splice new_node into list |
| 100 | + new_node.prev = left_prev |
| 101 | + new_node.next = right_next |
| 102 | + if left_prev is not None: |
| 103 | + left_prev.next = new_node |
| 104 | + if right_next is not None: |
| 105 | + right_next.prev = new_node |
| 106 | + |
| 107 | + # mark removed nodes dead |
| 108 | + left.alive = False |
| 109 | + right.alive = False |
| 110 | + |
| 111 | + # add contributions of new adjacent pairs to bad |
| 112 | + if left_prev is not None and left_prev.alive: |
| 113 | + if left_prev.val > new_node.val: |
| 114 | + bad += 1 |
| 115 | + # push (left_prev, new_node) into heap |
| 116 | + heapq.heappush(heap, (left_prev.val + new_node.val, left_prev.idx, id(left_prev), left_prev)) |
| 117 | + if right_next is not None and right_next.alive: |
| 118 | + if new_node.val > right_next.val: |
| 119 | + bad += 1 |
| 120 | + # push (new_node, right_next) into heap |
| 121 | + heapq.heappush(heap, (new_node.val + right_next.val, new_node.idx, id(new_node), new_node)) |
| 122 | + |
| 123 | + ops += 1 |
| 124 | + if bad == 0: |
| 125 | + return ops |
| 126 | + |
| 127 | + # also, new_node could pair with neither side if isolated (single element left), in which case |
| 128 | + # if both neighbors are None -> array size 1 -> it's non-decreasing; but bad would be 0 already. |
| 129 | + # No need to push pair for new_node with left_prev or right_next if neighbor missing. |
| 130 | + |
| 131 | + # Note: If new_node has both neighbors, both pushes above ensure future merges considered. |
| 132 | + # Should not reach here without bad == 0, but just in case: |
| 133 | + return ops |
| 134 | + |
| 135 | +# For LeetCode style usage: |
| 136 | +# class Solution: (the above is already in correct shape) |
| 137 | +# Example local test |
| 138 | +if __name__ == "__main__": |
| 139 | + sol = Solution() |
| 140 | + print(sol.minimumOperations([5,2,3,1])) # expected 2 |
| 141 | + print(sol.minimumOperations([1,2,2])) # expected 0 |
| 142 | +``` |
| 143 | + |
| 144 | +- Notes about the solution approach: |
| 145 | + - Use a doubly-linked list of nodes so that when we merge two adjacent nodes we can update neighbors in O(1) time. |
| 146 | + - Maintain a min-heap of adjacent-pair sums with tiebreak by the leftmost original index so we always pick the leftmost minimal-sum adjacent pair. |
| 147 | + - Keep a "bad" counter that tracks the number of adjacent inversions (pairs where left > right). Update only affected pairs when a merge happens, which is O(1) per merge. |
| 148 | + - Skip stale heap entries by validating that the left node is still alive and its next is the expected right node. |
| 149 | + - Time complexity: O(n log n) (at most n-1 merges, each causes at most O(log n) heap operations). Space complexity: O(n) for nodes and heap. |
0 commit comments