Problem overview
The method lastStoneWeight receives an Array[Int] of positive stone weights. Each turn the two heaviest stones are smashed together. If they weigh the same, both disappear. If they differ, the lighter one disappears and the heavier one is left with the difference of the two weights. The process repeats until at most one stone remains, and the method returns that stone's weight, or 0 if no stone is left.
For [2, 7, 4, 1, 8, 1] the smashes go like this:
- 8 and 7 leave a 1, so the stones are 2, 4, 1, 1, 1.
- 4 and 2 leave a 2, so the stones are 2, 1, 1, 1.
- 2 and 1 leave a 1, so the stones are 1, 1, 1.
- 1 and 1 destroy each other, leaving one stone of weight 1.
The answer is 1.
Approach: always grab the heaviest two
Each turn needs the two largest weights, and then a new, smaller weight goes back in. Sorting once isn't enough, because the new stone belongs somewhere in the middle of the order. Re-sorting after every smash would cost O(n log n) per turn. A max heap fits better: it returns its largest element quickly and accepts new elements quickly, each in O(log n).
So the plan is to put all stones in a max heap, then repeat: take the largest, take the next largest, and if they differ, put their difference back. Every turn removes at least one stone from the heap (two out, at most one in), so the loop ends after fewer than n turns.
Seen from the heap's side, the example looks like this, with the largest weight first:
start: 8 7 4 2 1 1
smash 8, 7: 4 2 1 1 1
smash 4, 2: 2 1 1 1
smash 2, 1: 1 1 1
smash 1, 1: 1
Each line is what a sorted view of the heap contains after that smash. The heap itself doesn't keep a fully sorted array, only enough order to find the largest element fast.
Step by step
- Create the heap. Declare
heapwithvalasPriorityQueue[Int](). Scala's mutablePriorityQueueis a max heap by default, so the heaviest stone is on top and no custom ordering is needed. - Load the stones with
heap.addAll(stones). It inserts every element of the array into the heap. - Loop while two stones remain:
while (heap.size >= 2). When the loop ends there are zero or one stones. - Take the heaviest stone into
val x = heap.dequeue().dequeue()removes the top value and returns it. - Take the second heaviest into
val y = heap.dequeue(). Sincexcame out first,x >= y. - Push the remainder. If
x != y, callheap.enqueue(x - y). Equal stones destroy each other, so nothing goes back. - Handle the empty case. After the loop, if
heap.size == 0, return 0.heap.isEmptymeans the same. - Otherwise return
heap.head, the last stone.headreads the top without removing it.
The solution in Scala
class Solution {
def lastStoneWeight(stones: Array[Int]): Int = {
val heap = PriorityQueue[Int]()
heap.addAll(stones)
while (heap.size >= 2) {
val x = heap.dequeue()
val y = heap.dequeue()
if (x != y) {
heap.enqueue(x - y)
}
}
if (heap.size == 0) {
return 0
}
return heap.head
}
}
The Scala details worth a closer look:
- The import.
PriorityQueueis inscala.collection.mutable. A standalone file needsimport scala.collection.mutable.PriorityQueueabove the class. - Calling the constructor without
new.PriorityQueue[Int]()works in Scala 3 and calls the companion'sapply. The queue gets its ordering from the implicitOrdering[Int], which puts bigger numbers first. For a min heap you would writePriorityQueue[Int]()(Ordering[Int].reverse). addAllcopies. Afterheap.addAll(stones), the heap holds its own copy of the values. The arraystonesis not changed, and later changes to the heap don't touch it.- Empty queues throw.
dequeue()andheadon an empty queue throwNoSuchElementException. The code is safe because the loop checksheap.size >= 2before the twodequeue()calls, and the finalifhandles an empty heap beforehead. - No
++or--. Not needed here, but remember that Scala has none. Use+= 1.
Complexity
Time: O(n log n). Loading the stones costs n insertions of O(log n) each. The loop runs fewer than n times, and each run does two removals and at most one insertion, each O(log n).
Space: O(n). The heap holds up to n stones.
Edge cases and common mistakes
- No stones. An empty array skips the loop and returns 0.
- One stone.
[5]skips the loop and returns 5. - Two equal stones.
[3, 3]returns 0. - Everything cancels.
[9, 3, 2, 10]returns 0: 10 and 9 leave 1, then 3 and 2 leave 1, and the two 1s cancel. - An odd number of equal stones.
[1, 1, 1]returns 1. - Pushing back zero. Without the
x != ytest, a stone of weight 0 would enter the heap, although the rules say equal stones are destroyed. It would also be counted byheap.sizeand cost extra turns. - Reading
headtoo early. Usingheadinstead ofdequeue()inside the loop leaves the stones in the heap, so the loop would never finish. - Printing the heap to debug. A
PriorityQueueprints its internal layout, not the sorted order, so printed output can look unsorted even when the heap is correct.
Testing
stones = [2, 7, 4, 1, 8, 1]gives1.stones = []gives0.stones = [5]gives5.stones = [3, 3]gives0.stones = [9, 3, 2, 10]gives0.
