LeetCode 1962 - Remove Stones to Minimize the Total
The problem gives us an array called piles, where each element represents the number of stones in a pile. We are also given an integer k, representing the exact number of operations we must perform. In one operation, we choose a pile and remove floor(pile / 2) stones from it.
Difficulty: 🟡 Medium
Topics: Array, Greedy, Heap (Priority Queue)
Solution
Problem Understanding
The problem gives us an array called piles, where each element represents the number of stones in a pile. We are also given an integer k, representing the exact number of operations we must perform.
In one operation, we choose a pile and remove floor(pile / 2) stones from it. Since we remove half of the pile rounded down, the remaining pile becomes:
$$pile - \left\lfloor \frac{pile}{2} \right\rfloor$$
This is equivalent to:
$$\left\lceil \frac{pile}{2} \right\rceil$$
For example:
- If a pile has
9stones, we remove4, leaving5 - If a pile has
6stones, we remove3, leaving3 - If a pile has
1stone, we remove0, so it stays1
The goal is to minimize the total number of stones remaining after exactly k operations.
The important detail is that we may choose the same pile multiple times. This means we are free to repeatedly reduce the currently largest pile if that is beneficial.
The constraints are large:
- Up to
10^5piles - Up to
10^5operations - Each pile may contain up to
10^4stones
Because both n and k can be very large, any solution that repeatedly scans the entire array for the maximum value will likely be too slow.
Several edge cases are important:
- A pile with value
1never changes becausefloor(1 / 2) = 0 kmay be larger than the number of piles, so we may revisit piles many times- The optimal choice at every step depends on the current largest pile, not the original largest pile
- Multiple piles may have the same size
Understanding that we always want the greatest immediate reduction is the key insight that leads to the optimal greedy solution.
Approaches
Brute Force Approach
A straightforward approach is to simulate all k operations directly.
For each operation:
- Scan the entire array to find the largest pile
- Remove
floor(pile / 2)stones from it - Update the pile
- Repeat
This works because removing stones from the largest pile always gives the maximum immediate reduction. However, finding the largest pile by scanning the array costs O(n) time each operation.
Since we perform k operations, the total complexity becomes:
$$O(n \times k)$$
With both n and k potentially equal to 10^5, this could require up to 10^{10} operations, which is far too slow.
Optimal Approach, Greedy + Max Heap
The key observation is that at every step, the best move is always to reduce the current largest pile.
Suppose we have two piles:
- Pile A = 20
- Pile B = 8
Reducing pile A removes 10 stones, while reducing pile B removes only 4 stones. Choosing the larger pile always gives the greatest reduction in the total sum.
This greedy choice remains optimal after every operation because the only thing that matters is maximizing the reduction at the current step.
To efficiently retrieve the largest pile repeatedly, we use a max heap.
A heap allows us to:
- Extract the largest pile in
O(log n) - Insert the updated pile in
O(log n)
This reduces the overall complexity dramatically.
Since Python's heapq is a min heap, we simulate a max heap by storing negative values.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(nk) | O(1) | Repeatedly scans entire array to find largest pile |
| Optimal | O((n + k) log n) | O(n) | Uses max heap for efficient largest-element retrieval |
Algorithm Walkthrough
- Compute the initial total number of stones.
Keeping track of the total lets us avoid summing the array again at the end. 2. Build a max heap containing all pile sizes.
Since Python only provides a min heap, we store negative values so the largest pile appears first.
3. Repeat the operation exactly k times.
In each iteration:
- Remove the largest pile from the heap
- Compute how many stones are removed:
$$removed = \left\lfloor \frac{largest}{2} \right\rfloor$$
- Subtract
removedfrom the running total - Compute the updated pile size:
$$newPile = largest - removed$$
- Push the updated pile back into the heap
- After all operations are complete, return the total.
Why it works
The greedy strategy is optimal because each operation is independent in terms of maximizing reduction. Removing stones from the largest pile always removes at least as many stones as removing from any smaller pile.
The heap guarantees that we always choose the current largest pile efficiently. Since every operation makes the locally optimal choice and no future operation can compensate for wasting a reduction opportunity earlier, the algorithm produces the minimum possible total.
Python Solution
from typing import List
import heapq
class Solution:
def minStoneSum(self, piles: List[int], k: int) -> int:
total_stones = sum(piles)
# Python heapq is a min heap, so use negative values
max_heap = [-pile for pile in piles]
heapq.heapify(max_heap)
for _ in range(k):
largest_pile = -heapq.heappop(max_heap)
removed_stones = largest_pile // 2
remaining_pile = largest_pile - removed_stones
total_stones -= removed_stones
heapq.heappush(max_heap, -remaining_pile)
return total_stones
The implementation begins by computing the total number of stones across all piles. This allows the algorithm to update the answer incrementally rather than recalculating the sum repeatedly.
Next, the code constructs a max heap. Because Python's heapq module only supports min heaps, each pile is stored as a negative number. This trick causes the largest original pile to appear as the smallest negative value.
Inside the loop, the algorithm removes the largest pile, computes how many stones are removed, updates the total, and inserts the reduced pile back into the heap.
The heap always maintains the current state of the piles, ensuring that each operation selects the largest available pile efficiently.
Go Solution
package main
import (
"container/heap"
)
type MaxHeap []int
func (h MaxHeap) Len() int {
return len(h)
}
func (h MaxHeap) Less(i, j int) bool {
return h[i] > h[j]
}
func (h MaxHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *MaxHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *MaxHeap) Pop() interface{} {
old := *h
n := len(old)
item := old[n-1]
*h = old[:n-1]
return item
}
func minStoneSum(piles []int, k int) int {
totalStones := 0
h := &MaxHeap{}
for _, pile := range piles {
totalStones += pile
*h = append(*h, pile)
}
heap.Init(h)
for i := 0; i < k; i++ {
largestPile := heap.Pop(h).(int)
removedStones := largestPile / 2
remainingPile := largestPile - removedStones
totalStones -= removedStones
heap.Push(h, remainingPile)
}
return totalStones
}
The Go implementation uses the container/heap package, which requires defining a custom heap type implementing the heap interface methods.
Unlike Python, Go allows us to directly define a max heap by customizing the Less function so that larger values have higher priority.
The rest of the logic mirrors the Python solution exactly.
Go integers are sufficient for this problem because the maximum possible total is:
$$10^5 \times 10^4 = 10^9$$
which fits safely inside a 32-bit signed integer.
Worked Examples
Example 1
Input:
piles = [5,4,9]
k = 2
Initial state:
| Step | Heap Content | Largest Pile | Removed | New Pile | Total |
|---|---|---|---|---|---|
| Start | [9,5,4] | - | - | - | 18 |
Operation 1:
- Extract
9 - Remove
9 // 2 = 4 - Remaining pile becomes
5
| Step | Heap Content | Largest Pile | Removed | New Pile | Total |
|---|---|---|---|---|---|
| After Op 1 | [5,5,4] | 9 | 4 | 5 | 14 |
Operation 2:
- Extract
5 - Remove
5 // 2 = 2 - Remaining pile becomes
3
| Step | Heap Content | Largest Pile | Removed | New Pile | Total |
|---|---|---|---|---|---|
| After Op 2 | [5,4,3] | 5 | 2 | 3 | 12 |
Final answer:
12
Example 2
Input:
piles = [4,3,6,7]
k = 3
Initial total:
20
Initial heap:
[7,6,4,3]
| Operation | Largest | Removed | Remaining | Total After |
|---|---|---|---|---|
| 1 | 7 | 3 | 4 | 17 |
| 2 | 6 | 3 | 3 | 14 |
| 3 | 4 | 2 | 2 | 12 |
Final answer:
12
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O((n + k) log n) | Heap construction costs O(n), each of the k operations performs heap pop and push |
| Space | O(n) | The heap stores all piles |
Building the heap initially takes linear time. Each operation removes and reinserts one pile, and both heap operations cost O(log n).
Since we perform exactly k operations, the total running time becomes:
$$O(n + k \log n)$$
The heap stores all pile values, requiring O(n) additional memory.
Test Cases
from typing import List
import heapq
class Solution:
def minStoneSum(self, piles: List[int], k: int) -> int:
total_stones = sum(piles)
max_heap = [-pile for pile in piles]
heapq.heapify(max_heap)
for _ in range(k):
largest_pile = -heapq.heappop(max_heap)
removed_stones = largest_pile // 2
remaining_pile = largest_pile - removed_stones
total_stones -= removed_stones
heapq.heappush(max_heap, -remaining_pile)
return total_stones
solution = Solution()
assert solution.minStoneSum([5, 4, 9], 2) == 12 # Example 1
assert solution.minStoneSum([4, 3, 6, 7], 3) == 12 # Example 2
assert solution.minStoneSum([1], 1) == 1 # Single pile that cannot shrink
assert solution.minStoneSum([10], 1) == 5 # Single operation on one pile
assert solution.minStoneSum([10], 2) == 3 # Repeated operations on same pile
assert solution.minStoneSum([10000], 100000) == 1 # Large k eventually stabilizes
assert solution.minStoneSum([2, 2, 2], 3) == 3 # Equal-sized piles
assert solution.minStoneSum([8, 1], 1) == 5 # Largest pile should be chosen
assert solution.minStoneSum([9, 8, 7], 0) == 24 # No operations
assert solution.minStoneSum([1, 1, 1, 1], 100) == 4 # All piles already minimal
assert solution.minStoneSum([20, 15, 10], 4) == 19 # Multiple greedy updates
Test Summary
| Test | Why |
|---|---|
[5,4,9], k=2 |
Validates first official example |
[4,3,6,7], k=3 |
Validates second official example |
[1], k=1 |
Tests pile that cannot shrink |
[10], k=1 |
Tests simple single reduction |
[10], k=2 |
Tests repeated operations on same pile |
[10000], k=100000 |
Tests very large k |
[2,2,2], k=3 |
Tests equal-sized piles |
[8,1], k=1 |
Ensures largest pile is selected |
[9,8,7], k=0 |
Tests no operations |
[1,1,1,1], k=100 |
Tests stable piles under many operations |
[20,15,10], k=4 |
Tests repeated greedy heap updates |
Edge Cases
One important edge case occurs when a pile contains only one stone. Since:
$$\left\lfloor \frac{1}{2} \right\rfloor = 0$$
the pile never changes. A buggy implementation might accidentally reduce it to zero or enter unnecessary processing loops. The heap-based solution handles this naturally because removing zero stones simply reinserts the value 1.
Another important case is when k is much larger than the number of piles. Since the same pile may be selected repeatedly, the algorithm must continue updating and reinserting piles correctly. The heap structure supports this naturally because every updated pile is pushed back for future consideration.
A third edge case involves multiple piles with identical sizes. The algorithm must still consistently choose one of the largest piles at each step. Since all equally large piles provide the same reduction amount, any tie-breaking order is valid. The heap implementation handles ties automatically without affecting correctness.
A final subtle edge case occurs when all piles become very small before operations finish. Eventually, every pile may become 1, after which no further reductions are possible. The algorithm still continues for the remaining operations, but each operation removes zero stones, leaving the total unchanged.