Problem overview
The method LastStoneWeight receives an array stones of positive weights. Each turn, the two heaviest stones are smashed together. If they weigh the same, both disappear. Otherwise the lighter one disappears and the heavier one stays, with the weight reduced by the lighter one's weight. After enough turns, at most one stone remains, and the method returns its weight, or 0 if no stone is left.
For [2, 7, 4, 1, 8, 1] the process looks like this:
- 8 and 7 smash into a stone of weight 1, leaving 2, 4, 1, 1, 1.
- 4 and 2 smash into 2, leaving 2, 1, 1, 1.
- 2 and 1 smash into 1, leaving 1, 1, 1.
- 1 and 1 destroy each other, leaving a single 1.
The answer is 1.
Approach: always pull out the two heaviest
The rules say what to do every turn: find the two heaviest stones, remove them, and possibly insert one new stone. Scanning the array for the two largest values on every turn costs O(n), and sorting it after every insertion costs even more.
A max heap fits exactly. It is a structure that always hands back its largest element, and both removing it and inserting a new element cost O(log n). The plan is to load every stone into the heap, then repeat: take out two stones, and if their weights differ, put the difference back. When fewer than two stones remain, the simulation is over.
Each turn removes at least one stone from the heap, so there are fewer than n turns. With O(log n) per heap operation, the total is O(n log n).
Step by step
- Create the heap. In C#,
PriorityQueue<int, int>stores an element together with a priority and gives back the element with the lowest priority first. A comparer that reverses the comparison,(x, y) => y - x, makes the heaviest stone come out first. - Loop over
stoneswithforeach, usingstoneas the loop variable. - Enqueue each stone.
heap.Enqueue(stone, stone)uses the stone's weight as both the element and its priority. - Loop while
heap.Count >= 2. A smash needs two stones. - Dequeue the heaviest stone into
x. - Dequeue the next heaviest into
y. Sincexcame out first,x >= y. - Put back the remainder. If
x != y, the stonex - ygoes back into the heap. If they are equal, nothing goes back, which means both stones are destroyed. - Handle the empty case. After the loop, if
heap.Count == 0, every stone has been destroyed, so return 0. - Return the survivor. Otherwise exactly one stone is left, and
heap.Peek()returns it without removing it.
The solution in C#
class Solution {
public int LastStoneWeight(int[] stones) {
PriorityQueue<int, int> heap = new PriorityQueue<int, int>(Comparer<int>.Create((x, y) => y - x));
foreach (int stone in stones) {
heap.Enqueue(stone, stone);
}
while (heap.Count >= 2) {
int x = heap.Dequeue();
int y = heap.Dequeue();
if (x != y) {
heap.Enqueue(x - y, x - y);
}
}
if (heap.Count == 0) {
return 0;
}
return heap.Peek();
}
}
Some C# details to notice:
- Elements and priorities are separate.
Enqueue(element, priority)takes two arguments, whileDequeue()andPeek()return only the element. Because every stone is its own priority, the element that comes out is also the weight. - The reversing comparer.
Comparer<int>.Createbuilds a comparer from a lambda. For two priorities, a negative result means the first should come out first.y - xis negative whenxis greater thany, so larger weights come out first. - Subtraction in comparers can overflow.
intarithmetic wraps silently, soy - xis wrong for extreme values such asint.MinValue. Positive weights avoid the problem, since the difference of two positive numbers always fits in anint. For general data,y.CompareTo(x)is the safe form. - Naming. The lambda parameters
xandyare separate from the local variablesxandyin the loop. They live in different scopes, and the lambda is only a short function passed toCreate. Countis a property. Writeheap.Countwithout parentheses.Dequeue()andPeek()are methods.- Empty queues throw.
Dequeue()orPeek()on an emptyPriorityQueuethrows anInvalidOperationException. That is why the code checksCountbefore every use.
Complexity
Time: O(n log n). Loading the heap takes n insertions of O(log n) each. The loop runs fewer than n times, because each turn removes two stones and returns at most one. Every turn performs two removals and at most one insertion, all O(log n).
Space: O(n). The heap holds up to n stones at the start.
Edge cases and common mistakes
- Everything cancels out. For
[3, 3]the two stones destroy each other, nothing is put back, and the method returns 0. - A single stone. For
[5]the loop never runs, andPeek()returns 5. - No stones. For an empty array the heap is empty and the method returns 0. Without the
Count == 0check,Peek()would throw. - Forgetting to reverse the order. A
PriorityQueuecreated without a comparer is a min heap, and the method would smash the two lightest stones. - Putting back the wrong value. Enqueue the difference,
x - y, with the same value as priority. Using the old weight as the priority would keep the new stone in the wrong place in the order. - Swapped roles. Because the heap returns the heavier stone first,
x - yis never negative. If you dequeue in the other order, you would need an absolute value.
Testing
stones = [2, 7, 4, 1, 8, 1]gives1.stones = [3, 3]gives0.stones = [5]gives5.stones = []gives0.