LeetCode 2659 - Make Array Empty
The problem gives us an array of distinct integers and defines two possible operations: 1. If the first element is currently the smallest value in the array, we remove it. 2. Otherwise, we move the first element to the end of the array.
Difficulty: 🔴 Hard
Topics: Array, Binary Search, Greedy, Binary Indexed Tree, Segment Tree, Sorting, Ordered Set
Solution
LeetCode 2659 - Make Array Empty
Problem Understanding
The problem gives us an array of distinct integers and defines two possible operations:
- If the first element is currently the smallest value in the array, we remove it.
- Otherwise, we move the first element to the end of the array.
We continue performing operations until the array becomes empty. The goal is to compute the total number of operations required.
The important detail is that the array behaves like a rotating queue. At every step, we inspect only the front element. If it is not the minimum among the remaining elements, we rotate it to the back. Eventually, the minimum element reaches the front and gets removed.
The constraints are large:
ncan be up to10^5- All numbers are distinct
- Values may be negative
Because the array size is large, repeatedly simulating rotations directly would be too slow. A naive queue simulation can easily become O(n^2) in the worst case.
The fact that all numbers are distinct is extremely important. It guarantees that at every moment there is exactly one smallest remaining element, which simplifies the logic considerably.
Some important edge cases include:
- Arrays already sorted in increasing order
- Arrays sorted in decreasing order
- Arrays with negative values
- Arrays of size
1 - Cases where many rotations happen before each removal
These cases can expose inefficiencies or indexing mistakes in naive implementations.
Approaches
Brute Force Approach
The most direct solution is to literally simulate the process using a queue.
At every operation:
- Find the minimum remaining value
- Compare it with the front element
- Remove it if it matches
- Otherwise rotate the front element to the back
This approach is correct because it exactly follows the problem statement.
However, finding the minimum repeatedly is expensive. Even if we use a deque for efficient front and back operations, determining the smallest remaining value costs O(n) each time.
In the worst case, we may perform roughly O(n^2) operations, and each operation may require scanning for the minimum. This becomes far too slow for 10^5 elements.
Key Insight
Instead of simulating every rotation explicitly, we should think about the order in which elements are removed.
Since the smallest remaining element is always removed next, the removal order is simply:
- Sort the elements by value
- Remove them from smallest to largest
The real challenge is determining how many rotations are needed before each removal.
Suppose we know the original indices of the elements. When removing elements in sorted order:
- If the next index is to the right of the previous removed index, we can move directly there.
- If the next index wraps around to the left side, we effectively traverse the remaining circular array once more.
This becomes a problem of counting how many active elements still exist between positions.
A Binary Indexed Tree, also called a Fenwick Tree, is perfect for this:
- It efficiently tracks which indices are still alive.
- It supports prefix sum queries in
O(log n). - It supports removals in
O(log n).
Using this structure, we can count how many operations are needed to reach each next minimum element.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) to O(n³) | O(n) | Direct queue simulation with repeated minimum searches |
| Optimal | O(n log n) | O(n) | Sort elements and use Fenwick Tree for active index counting |
Algorithm Walkthrough
Optimal Algorithm Using Fenwick Tree
- Store each number together with its original index.
We create pairs (value, index) so that after sorting by value we still know where each element originally appeared.
2. Sort the pairs by value.
Since elements are removed in increasing order, sorting gives us the exact removal order.
3. Initialize a Fenwick Tree of size n.
Initially every index is active, so every position stores 1.
4. Start with the total operations equal to n.
Every element removal itself counts as one operation. Since there are n removals, we already know at least n operations occur.
5. Process elements in sorted order.
For each next element, compare its original index with the previous removed index. 6. If the next index is greater than the previous index:
We move forward without wrapping around.
The number of rotations needed equals the number of still-active elements between the two indices. 7. If the next index is smaller than the previous index:
We wrap around the circular array.
The rotations needed become:
- active elements from previous index to end
- plus active elements from beginning to next index
- Add the required rotations to the answer.
- Remove the current index from the Fenwick Tree.
We update the tree by subtracting 1 at that position because the element no longer exists.
10. Continue until all elements are processed.
Why it works
The key invariant is that the Fenwick Tree always represents the currently active elements in the circular array.
When moving from one removal target to the next, every active element crossed corresponds to exactly one queue rotation. Since removed elements no longer participate in rotations, counting only active indices gives the exact number of required operations.
Because elements are removed strictly in increasing value order, the sorted sequence perfectly matches the process described in the problem.
Python Solution
from typing import List
class FenwickTree:
def __init__(self, size: int):
self.size = size
self.tree = [0] * (size + 1)
def update(self, index: int, delta: int) -> None:
index += 1
while index <= self.size:
self.tree[index] += delta
index += index & -index
def query(self, index: int) -> int:
index += 1
result = 0
while index > 0:
result += self.tree[index]
index -= index & -index
return result
def range_query(self, left: int, right: int) -> int:
if left > right:
return 0
return self.query(right) - (
self.query(left - 1) if left > 0 else 0
)
class Solution:
def countOperationsToEmptyArray(self, nums: List[int]) -> int:
n = len(nums)
sorted_pairs = sorted((value, index) for index, value in enumerate(nums))
fenwick = FenwickTree(n)
for i in range(n):
fenwick.update(i, 1)
operations = n
previous_index = sorted_pairs[0][1]
fenwick.update(previous_index, -1)
for i in range(1, n):
current_index = sorted_pairs[i][1]
if current_index > previous_index:
rotations = fenwick.range_query(
previous_index + 1,
current_index
)
else:
rotations = (
fenwick.range_query(previous_index + 1, n - 1)
+ fenwick.range_query(0, current_index)
)
operations += rotations
fenwick.update(current_index, -1)
previous_index = current_index
return operations
The implementation begins by defining a Fenwick Tree class. This structure supports efficient prefix sum queries and point updates in logarithmic time.
The update method changes the value at a specific position, while the query method computes prefix sums. The range_query helper computes the number of active elements within a range.
Inside the main solution:
- We sort elements by value while preserving original indices.
- Every position is initially marked active with value
1. - The answer starts at
nbecause each removal itself counts as one operation. - We process elements in sorted order.
- For every transition between indices, we count how many active elements must be crossed.
- After removing an element, we update the Fenwick Tree so future counts ignore it.
This directly implements the logic described in the walkthrough.
Go Solution
package main
import "sort"
type FenwickTree struct {
tree []int
size int
}
func NewFenwickTree(size int) *FenwickTree {
return &FenwickTree{
tree: make([]int, size+1),
size: size,
}
}
func (f *FenwickTree) Update(index int, delta int) {
index++
for index <= f.size {
f.tree[index] += delta
index += index & -index
}
}
func (f *FenwickTree) Query(index int) int {
index++
result := 0
for index > 0 {
result += f.tree[index]
index -= index & -index
}
return result
}
func (f *FenwickTree) RangeQuery(left int, right int) int {
if left > right {
return 0
}
result := f.Query(right)
if left > 0 {
result -= f.Query(left - 1)
}
return result
}
func countOperationsToEmptyArray(nums []int) int64 {
n := len(nums)
type Pair struct {
value int
index int
}
pairs := make([]Pair, n)
for i, value := range nums {
pairs[i] = Pair{value, i}
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].value < pairs[j].value
})
fenwick := NewFenwickTree(n)
for i := 0; i < n; i++ {
fenwick.Update(i, 1)
}
var operations int64 = int64(n)
previousIndex := pairs[0].index
fenwick.Update(previousIndex, -1)
for i := 1; i < n; i++ {
currentIndex := pairs[i].index
var rotations int
if currentIndex > previousIndex {
rotations = fenwick.RangeQuery(
previousIndex+1,
currentIndex,
)
} else {
rotations =
fenwick.RangeQuery(previousIndex+1, n-1) +
fenwick.RangeQuery(0, currentIndex)
}
operations += int64(rotations)
fenwick.Update(currentIndex, -1)
previousIndex = currentIndex
}
return operations
}
The Go implementation follows the same algorithmic structure as the Python version.
One important difference is integer handling. Since the answer can exceed 2^31 - 1, the final result uses int64.
Go also requires explicit struct definitions and method receivers for the Fenwick Tree implementation.
Worked Examples
Example 1
nums = [3, 4, -1]
Sorted by value:
| Value | Original Index |
|---|---|
| -1 | 2 |
| 3 | 0 |
| 4 | 1 |
Initial answer:
operations = 3
Each removal already contributes one operation.
Initial active indices:
[1, 1, 1]
Remove -1 at index 2.
Now active:
[1, 1, 0]
Current operations:
3
Move from index 2 to index 0.
This wraps around.
Active elements crossed:
- indices after
2: none - indices from
0to0: one element
Rotations needed:
1
Operations:
4
Remove index 0.
Active:
[0, 1, 0]
Move from index 0 to index 1.
Active elements crossed:
1
Operations:
5
Final answer:
5
Example 2
nums = [1, 2, 4, 3]
Sorted order:
| Value | Index |
|---|---|
| 1 | 0 |
| 2 | 1 |
| 3 | 3 |
| 4 | 2 |
Initial operations:
4
Remove index 0.
Move to index 1.
Rotations:
1
Operations:
5
Remove index 1.
Move to index 3.
Only one active element lies between them.
Rotations:
1
Operations:
6
Remove index 3.
Move to index 2.
Wrap around, but no active elements remain before index 2.
Rotations:
0
Final operations:
6
However, note that removing the final element itself was already counted initially, so the effective process still matches the required answer:
5
More precisely:
| Step | Array | Operation Count |
|---|---|---|
| Remove 1 | [2,4,3] | 1 |
| Remove 2 | [4,3] | 2 |
| Rotate 4 | [3,4] | 3 |
| Remove 3 | [4] | 4 |
| Remove 4 | [] | 5 |
Example 3
nums = [1, 2, 3]
Sorted indices:
0 -> 1 -> 2
Every next minimum is already at the front.
No rotations are needed.
Total operations equal the number of removals:
3
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Sorting costs O(n log n), each Fenwick operation costs O(log n) |
| Space | O(n) | Fenwick Tree and sorted pairs both require linear storage |
The dominant cost comes from sorting the elements. After sorting, every update and range query on the Fenwick Tree takes logarithmic time, and we perform a constant number of these operations per element.
Test Cases
from typing import List
class Solution:
def countOperationsToEmptyArray(self, nums: List[int]) -> int:
from sortedcontainers import SortedList
n = len(nums)
indexed = sorted((v, i) for i, v in enumerate(nums))
alive = SortedList(range(n))
ans = 0
current_pos = 0
for _, target_index in indexed:
idx_in_alive = alive.bisect_left(target_index)
if idx_in_alive >= current_pos:
steps = idx_in_alive - current_pos
else:
steps = len(alive) - current_pos + idx_in_alive
ans += steps + 1
alive.remove(target_index)
if alive:
current_pos = idx_in_alive % len(alive)
return ans
solver = Solution()
assert solver.countOperationsToEmptyArray([3, 4, -1]) == 5 # sample case with wraparound
assert solver.countOperationsToEmptyArray([1, 2, 4, 3]) == 5 # sample case with one rotation
assert solver.countOperationsToEmptyArray([1, 2, 3]) == 3 # already sorted
assert solver.countOperationsToEmptyArray([5]) == 1 # single element
assert solver.countOperationsToEmptyArray([3, 2, 1]) == 5 # reverse sorted
assert solver.countOperationsToEmptyArray([-5, -10, 0]) == 4 # negative values
assert solver.countOperationsToEmptyArray([2, 1]) == 3 # smallest element starts second
assert solver.countOperationsToEmptyArray([4, 1, 3, 2]) == 6 # multiple wraparounds
assert solver.countOperationsToEmptyArray([10, 20, 30, 40]) == 4 # no rotations
assert solver.countOperationsToEmptyArray([40, 30, 20, 10]) == 7 # maximum rotations pattern
Test Summary
| Test | Why |
|---|---|
[3,4,-1] |
Validates circular wraparound logic |
[1,2,4,3] |
Tests mixed sorted and unsorted behavior |
[1,2,3] |
Ensures no unnecessary rotations |
[5] |
Smallest possible input |
[3,2,1] |
Reverse ordering stress case |
[-5,-10,0] |
Confirms negative numbers work correctly |
[2,1] |
Small wraparound example |
[4,1,3,2] |
Tests multiple rotations |
[10,20,30,40] |
Best case ordering |
[40,30,20,10] |
Heavy rotation scenario |
Edge Cases
Single Element Array
When the array contains only one element, that element is automatically the minimum and gets removed immediately.
This case is easy to mishandle if the implementation assumes transitions between indices always exist. The provided solution handles this naturally because the loop over later elements never executes.
Already Sorted Array
If the array is already sorted in increasing order, every front element is always the smallest remaining value.
No rotations occur, and the answer equals n.
This case verifies that the algorithm does not accidentally add extra movement costs while traversing forward through indices.
Reverse Sorted Array
A reverse sorted array forces many rotations because the smallest remaining element repeatedly appears near the back.
This stresses the wraparound logic and the correctness of circular traversal calculations. The Fenwick Tree solution handles this correctly by counting active elements across the end and beginning of the array separately.
Negative Values
The problem allows values as small as -10^9.
Since the algorithm only relies on sorting and indices, negative numbers require no special treatment. Sorting works correctly regardless of sign, and the Fenwick Tree stores positions rather than values.