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.

LeetCode Problem 2659

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:

  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.

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:

  • n can be up to 10^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

  1. 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
  1. Add the required rotations to the answer.
  2. 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 n because 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 0 to 0: 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.