1046. Last Stone Weight - Explanation

Problem Link

Description

You are given an array of integers stones where stones[i] represents the weight of the ith stone.

We want to run a simulation on the stones as follows:

  • At each step we choose the two heaviest stones, with weight x and y and smash them togethers
  • If x == y, both stones are destroyed
  • If x < y, the stone of weight x is destroyed, and the stone of weight y has new weight y - x.

Continue the simulation until there is no more than one stone remaining.

Return the weight of the last remaining stone or return 0 if none remain.

Example 1:

Input: stones = [2,3,6,2,4]

Output: 1

Explanation:
We smash 6 and 4 and are left with a 2, so the array becomes [2,3,2,2].
We smash 3 and 2 and are left with a 1, so the array becomes [1,2,2].
We smash 2 and 2, so the array becomes [1].

Example 2:

Input: stones = [1,2]

Output: 1

Constraints:

  • 1 <= stones.length <= 20
  • 1 <= stones[i] <= 100


Topics

Recommended Time & Space Complexity

You should aim for a solution as good or better than O(nlogn) time and O(n) space, where n is the size of the input array.


Hint 1

A naive solution would involve simulating the process by sorting the array at each step and processing the top 2 heaviest stones, resulting in an O(n * nlogn) time complexity. Can you think of a better way? Consider using a data structure that efficiently supports insertion and removal of elements and maintain the sorted order.


Hint 2

We can use a Max-Heap, which allows us to retrieve the maximum element in O(1) time. We initially insert all the weights into the Max-Heap, which takes O(logn) time per insertion. We then simulate the process until only one or no element remains in the Max-Heap. At each step, we pop two elements from the Max-Heap which takes O(logn) time. If they are equal, we do not insert anything back into the heap and continue. Otherwise, we insert the difference of the two elements back into the heap.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Heap / Priority Queue - Efficiently retrieving and removing the maximum (or minimum) element
  • Sorting - Sorting arrays to process elements in order
  • Simulation - Implementing step-by-step processes as described in problem statements

1. Sorting

Intuition

You always need to smash the two heaviest stones together.
A simple way to ensure this is:

  1. Sort the list of stones so the heaviest stones are at the end.
  2. Remove the last two stones (the largest values).
  3. Smash them:
    • If they are equal → both disappear.
    • If they are different → the difference becomes a new stone.
  4. Insert the new stone (if any) back into the list.
  5. Repeat until at most one stone remains.

Sorting each time is not the most efficient approach, but it is straightforward and easy to implement.

Algorithm

  1. While the number of stones is greater than 1:

    • Sort the stones in increasing order.
    • Remove the two largest stones:
      • Let a = largest stone
      • Let b = second largest stone
    • Compute difference = a - b
    • If difference > 0, insert the new stone back into the list.
  2. If the list is empty, return 0
    Else return the remaining stone.

class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:

        while len(stones) > 1:
            stones.sort()
            cur = stones.pop() - stones.pop()
            if cur:
                stones.append(cur)

        return stones[0] if stones else 0

Time & Space Complexity

  • Time complexity: O(n2logn)O(n ^ 2 \log n)
  • Space complexity: O(1)O(1) or O(n)O(n) depending on the sorting algorithm.

Intuition

We always smash the two heaviest stones.
If we keep the stones in sorted order, the two heaviest are at the end of the array, so we can pick them easily.

After smashing:

  • We remove the two heaviest stones.
  • If they are different, their difference becomes a new stone.
  • To keep the array sorted, we need to insert this new stone at the correct position.

Instead of scanning linearly to find the position, we use binary search to quickly find where this new stone should go in the sorted list, and then shift elements to insert it there. This keeps the list sorted for the next iteration.

Algorithm

  1. Sort the array of stones in non-decreasing order.
  2. Let n be the current number of stones.
  3. While n > 1:
    1. Take the last two stones (the two heaviest).
    2. Compute cur = heaviest1 - heaviest2.
    3. Decrease n by 2 (since we used two stones).
    4. If cur > 0:
      • Use binary search on the first n elements to find the position pos where cur should be inserted to keep the array sorted.
      • Increase n by 1.
      • Shift elements from the end to pos one step to the right.
      • Place cur at index pos.
  4. After the loop:
    • If n == 1, return the remaining stone.
    • If n == 0, return 0.
class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:
        stones.sort()
        n = len(stones)

        while n > 1:
            cur = stones.pop() - stones.pop()
            n -= 2
            if cur > 0:
                l, r = 0, n
                while l < r:
                    mid = (l + r) // 2
                    if stones[mid] < cur:
                        l = mid + 1
                    else:
                        r = mid
                pos = l
                n += 1
                stones.append(0)
                for i in range(n - 1, pos, -1):
                    stones[i] = stones[i - 1]
                stones[pos] = cur

        return stones[0] if n > 0 else 0

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(1)O(1) or O(n)O(n) depending on the sorting algorithm.

3. Heap

Intuition

We always need to repeatedly remove the two heaviest stones.
A max-heap is perfect for this because it lets us efficiently extract the largest values.

Most languages provide min-heaps, so a common trick is to store negative values.
This makes the smallest (most negative) value represent the largest stone.

Process:

  1. Convert all stones to negative and build a heap.
  2. Repeatedly pop the two smallest (i.e., the two heaviest stones).
  3. Smash them:
    • If equal → both are destroyed.
    • If different → push the negative of their difference back into the heap.
  4. When one or zero stones remain, return the remaining weight or 0.

Algorithm

  1. Convert every stone weight x into -x and build a min-heap.
  2. While the heap contains more than one stone:
    • Pop two values a and b (these represent the two heaviest stones).
    • If a != b, compute the remaining stone weight:
      • diff = a - b (still negative)
      • Push diff back into the heap.
  3. After the loop:
    • If the heap is empty, return 0.
    • Otherwise return the absolute value of the remaining stone.
class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:
        stones = [-s for s in stones]
        heapq.heapify(stones)

        while len(stones) > 1:
            first = heapq.heappop(stones)
            second = heapq.heappop(stones)
            if second > first:
                heapq.heappush(stones, first - second)

        stones.append(0)
        return abs(stones[0])

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

4. Bucket Sort

Intuition

Since all stone values lie within a limited numeric range, we can avoid sorting or using a heap by using bucket sort / frequency counting.

Instead of tracking every stone individually, we store how many stones exist for each possible weight.

Key ideas:

  • Let bucket[w] store how many stones of weight w we have.
  • We repeatedly look for the heaviest available stone.
  • When smashing stones of weights a and b:
    • If a == b, they cancel out.
    • If different, the leftover stone a - b gets added back into the bucket.
  • We continue until only one non-zero weight remains.

This works because bucket operations (increment, decrement, scanning) are efficient when the weight range is manageable.

Algorithm

  1. Find the maximum stone weight.
  2. Create a frequency array bucket of size maxWeight + 1.
  3. Count how many stones of each weight exist.
  4. Start from the heaviest weight and repeat:
    • If the count at this weight is even, all stones cancel in pairs.
    • If odd, one stone remains; find the next heaviest stone to smash with it.
    • Compute the difference and update the bucket.
    • Move the pointer to the next heaviest relevant weight.
  5. When only one weight remains with an odd count, return it.
  6. If all stones cancel out, return 0.
class Solution:
    def lastStoneWeight(self, stones: List[int]) -> int:

        maxStone = max(stones)
        bucket = [0] * (maxStone + 1)
        for stone in stones:
            bucket[stone] += 1

        first = second = maxStone
        while first > 0:
            if bucket[first] % 2 == 0:
                first -= 1
                continue

            j = min(first - 1, second)
            while j > 0 and bucket[j] == 0:
                j -= 1

            if j == 0:
                return first
            second = j
            bucket[first] -= 1
            bucket[second] -= 1
            bucket[first - second] += 1
            first = max(first - second, second)
        return first

Time & Space Complexity

  • Time complexity: O(n+w)O(n + w)
  • Space complexity: O(w)O(w)

Where nn is the length of the stonesstones array and ww is the maximum value in the stonesstones array.


Common Pitfalls

Using a Min-Heap Instead of Max-Heap

Many languages provide min-heaps by default (like Python's heapq). You need to either use a max-heap or negate the values when inserting and extracting. Forgetting this results in smashing the two smallest stones instead of the two heaviest.

Forgetting to Handle the Empty Result Case

After all smashing is complete, the stones array could be empty (all stones canceled out). Returning stones[0] without checking if the array is empty causes an index error. Always check if the result is empty and return 0 in that case.