Problem overview
The function lastStoneWeight receives a slice stones of positive integer 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 stays with the difference as its new weight. The game ends when at most one stone is left, and the function returns that stone's weight, or 0 if no stone is left.
In the example, stones = []int{2, 7, 4, 1, 8, 1}:
- 8 and 7 are smashed and leave a stone of weight 1, so the stones are 2, 4, 1, 1, 1.
- 4 and 2 leave 2, so the stones are 2, 1, 1, 1.
- 2 and 1 leave 1, so the stones are 1, 1, 1.
- 1 and 1 destroy each other, which leaves a single 1.
The answer is 1.
Approach: always take the two largest
The process says exactly what to do each turn: find the two heaviest stones, remove them, and put back their difference if it isn't zero. The question is how to find the heaviest stones quickly.
Sorting after every smash costs O(n log n) per turn, and the game can last n - 1 turns. Scanning for the two largest each time is O(n) per turn. A max heap is better suited: it gives the largest element in O(1) and removes it, or inserts a new element, in O(log n).
The plan:
- Turn the slice into a max heap.
- While at least two stones remain, pop the two largest,
xtheny. Sincexcame out first,x >= y. - If
x != y, pushx - yback. If they are equal, both stones are gone and nothing is pushed. - Each turn removes at least one stone, so the loop ends. At most one stone remains: return it, or 0 if the heap is empty.
Step by step
- Convert
stonesto amaxHeapnamedstoneHeapand callheap.Init(&stoneHeap). ThemaxHeaptype is provided by the lesson: a slice of ints that implementsheap.InterfacewithLessreversed, so the largest value is on top.heap.Initrearranges the whole slice into heap order inO(n), which is cheaper than pushing the stones one at a time. - Loop while there are at least two stones:
for stoneHeap.Len() >= 2. Go has nowhile, and aforwith only a condition does the job. - Pop the heaviest stone into
x.heap.Popreturns anany, so the type assertion.(int)turns it back into anint. - Pop the next heaviest into
ythe same way. - Push the difference if there is one. If
x != y, callheap.Push(&stoneHeap, x - y). Use the package function, not the method on the type, so the heap order is restored. - Handle the empty case. After the loop, if
stoneHeap.Len() == 0, return 0. - Return the last stone. Otherwise exactly one stone is left, and it sits at index 0, the root of the heap.
The solution in Go
The solution uses the provided maxHeap type together with the container/heap package. A typical definition of that type is:
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 any) { *h = append(*h, x.(int)) }
func (h *maxHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
And the function:
func lastStoneWeight(stones []int) int {
stoneHeap := maxHeap(stones)
heap.Init(&stoneHeap)
for stoneHeap.Len() >= 2 {
x := heap.Pop(&stoneHeap).(int)
y := heap.Pop(&stoneHeap).(int)
if x != y {
heap.Push(&stoneHeap, x - y)
}
}
if stoneHeap.Len() == 0 {
return 0
}
return stoneHeap[0]
}
The Go details worth knowing:
- Type conversion shares memory.
maxHeap(stones)converts a[]intto the named type with the same underlying slice. Nothing is copied, soheap.Initand the later pops rearrange and overwrite the caller's slice. After the example call, the caller'sstonesreads[1 1 1 2 4 8]. Copy the slice first if the caller needs the original order. - Why
&stoneHeap.PushandPopchange the slice's length, so they have pointer receivers, andheap.Init,heap.Popandheap.Pushall take the pointer.stoneHeap.Len()andstoneHeap[0]work on the variable directly. - Package functions, not methods. The type's own
PushandPoponly append or cut the last element. The functionsheap.Pushandheap.Popcall them and restore the heap order. - Type assertion.
heap.Pop(...).(int)panics if the stored value isn't anint. Here it always is. - Index 0 is the root. In a heap stored in a slice, the top is at the front. When only one element is left, it is the answer.
Complexity
Let n be the number of stones.
Time: O(n log n). Building the heap with heap.Init takes O(n). Every turn removes at least one stone, so there are at most n - 1 turns. Each turn has two pops and at most one push, each O(log n).
Space: O(1) extra. The heap is built inside the input slice. The loop pushes at most one stone after popping two, so the slice never needs to grow.
Edge cases and common mistakes
- Empty slice. The loop doesn't run, the length is 0, and the function returns 0.
- One stone.
stones = []int{9}returns 9, because there is nothing to smash. - Two equal stones.
stones = []int{3, 3}returns 0. - Several rounds.
stones = []int{10, 4, 2, 10}returns 2: the two 10s cancel, then 4 and 2 leave 2. - Pushing zero. Pushing
x - yeven when it is zero would leave a phantom stone of weight 0 in the heap, and the length checks would be wrong. Theif x != yguard prevents it. - Using the heap as a min heap. If
Lessused<, the lightest stones would be smashed first. - Popping with the method.
stoneHeap.Pop()removes the last slice element, not the largest stone. - Forgetting
heap.Init. The slice is in arbitrary order, so the first pops would not return the heaviest stones.
Testing
stones = []int{2, 7, 4, 1, 8, 1}gives 1.stones = []int{3, 3}gives 0.stones = []int{9}gives 9.stones = []int{10, 4, 2, 10}gives 2.stones = []int{}gives 0.