title: Last Stone Weight slug: last-stone-weight difficulty: easy leetcode_id: 1046 leetcode_url: https://leetcode.com/problems/last-stone-weight/ categories: - arrays - heap patterns: - heap function_signature: "def last_stone_weight(stones: list[int]) -> int:" test_cases: visible: - input: { stones: [2, 7, 4, 1, 8, 1] } expected: 1 - input: { stones: [1] } expected: 1 hidden: - input: { stones: [2, 2] } expected: 0 - input: { stones: [1, 3] } expected: 2 - input: { stones: [10, 4, 2, 10] } expected: 2 - input: { stones: [1, 1, 1, 1, 1] } expected: 1 - input: { stones: [5, 5, 5, 5] } expected: 0 - input: { stones: [1000] } expected: 1000 description: | You are given an array of integers `stones` where `stones[i]` is the weight of the ith stone. We are playing a game with the stones. On each turn, we choose the **heaviest two stones** and smash them together. Suppose the heaviest two stones have weights `x` and `y` with `x <= y`. The result of this smash is: - If `x == y`, both stones are destroyed, and - If `x != y`, the stone of weight `x` is destroyed, and the stone of weight `y` has new weight `y - x`. At the end of the game, there is **at most one** stone left. Return *the weight of the last remaining stone*. If there are no stones left, return `0`. constraints: | - `1 <= stones.length <= 30` - `1 <= stones[i] <= 1000` examples: - input: "stones = [2,7,4,1,8,1]" output: "1" explanation: "We combine 7 and 8 to get 1 so the array converts to [2,4,1,1,1], then we combine 2 and 4 to get 2 so the array converts to [2,1,1,1], then we combine 2 and 1 to get 1 so the array converts to [1,1,1], then we combine 1 and 1 to get 0 so the array converts to [1]. That's the value of the last stone." - input: "stones = [1]" output: "1" explanation: "There is only one stone, so we return its weight directly." explanation: intuition: | Imagine you have a collection of rocks and you keep smashing the two largest ones together. After each collision, either both rocks disappear (if equal weight) or you're left with a smaller rock (the difference in weights). The key insight is that we always need quick access to the **two heaviest stones**. After smashing, we might need to put a new stone back and find the next two heaviest. This screams for a **max heap** (priority queue) — a data structure designed for exactly this: efficiently finding and removing the maximum element. Think of it like this: the heap is a "smart pile" that always keeps the biggest stone on top. When you grab the top two stones, smash them, and toss the result back in, the pile automatically rearranges to put the new biggest stone on top. Without a heap, you'd have to re-sort the array after each smash, which is inefficient. The heap lets you do the same thing in logarithmic time per operation. approach: | We solve this using a **Max Heap** approach: **Step 1: Build a max heap from the stones** - Python's `heapq` module implements a *min* heap, so we negate all values to simulate a max heap - Use `heapify()` to convert the list into a heap in O(n) time   **Step 2: Simulate the smashing process** - While the heap has more than one stone: - Pop the two largest stones (negate to get actual values) - If they're not equal, push the difference back (negated) - If they're equal, both are destroyed (don't push anything back)   **Step 3: Return the result** - If the heap is empty, return `0` (all stones destroyed each other) - Otherwise, return the remaining stone's weight (negated back to positive)   This simulation directly follows the problem rules, and the heap ensures we always grab the two heaviest stones efficiently. common_pitfalls: - title: Using Sort Instead of Heap description: | A tempting approach is to sort the array, take the two largest, and re-sort after each operation. While this works, it's inefficient: - Sorting takes O(n log n) per smash - With up to n smashes, total time becomes O(n^2 log n) The heap approach does each operation in O(log n), giving O(n log n) total. For this problem's small constraints (n <= 30), sorting works fine, but heaps are the right tool for this pattern. wrong_approach: "Re-sorting after each smash" correct_approach: "Use a max heap for O(log n) insert/extract" - title: Forgetting Python Uses Min Heap description: | Python's `heapq` is a min heap, not a max heap. If you push positive values, `heappop()` gives you the *smallest* element, not the largest. The fix is to negate values: push `-stone` and negate again when popping. This "flips" the ordering so the largest original value becomes the smallest negated value (and thus pops first). wrong_approach: "Using heapq with positive values" correct_approach: "Negate values to simulate max heap" - title: Not Handling the Empty Heap Case description: | When all stones perfectly cancel out (e.g., `[2, 2]`), the heap becomes empty. You must check if the heap is empty before trying to return the last element. Returning `0` when the heap is empty is specified in the problem: "If there are no stones left, return `0`." key_takeaways: - "**Max heap pattern**: When you need repeated access to the maximum (or minimum) element with insertions, use a heap" - "**Python heap trick**: Negate values to convert `heapq` (min heap) into a max heap" - "**Simulation problems**: Sometimes the solution is just carefully implementing the rules with the right data structure" - "**Foundation for harder problems**: This pattern extends to problems like merging stones with costs, scheduling, or any greedy selection of extremes" time_complexity: "O(n log n). We perform at most `n` heap operations (pop and push), and each operation takes O(log n) time." space_complexity: "O(n). We store all stones in the heap initially. In-place heapify uses no extra space beyond the input." solutions: - approach_name: Max Heap is_optimal: true code: | import heapq def last_stone_weight(stones: list[int]) -> int: # Negate values to simulate max heap (Python's heapq is min heap) heap = [-s for s in stones] heapq.heapify(heap) # Smash stones until one or none remain while len(heap) > 1: # Pop two heaviest stones (negate to get actual values) first = -heapq.heappop(heap) second = -heapq.heappop(heap) # If they're not equal, push the difference back if first != second: heapq.heappush(heap, -(first - second)) # Return last stone or 0 if none left return -heap[0] if heap else 0 explanation: | **Time Complexity:** O(n log n) — Each of the up to n-1 smash operations involves two pops and at most one push, each O(log n). **Space Complexity:** O(n) — The heap stores all n stones initially. We use a max heap to efficiently find and remove the two heaviest stones. After each smash, if there's a remainder, we push it back. The process continues until at most one stone remains. - approach_name: Sorting (Simulation) is_optimal: false code: | def last_stone_weight(stones: list[int]) -> int: # Keep smashing until one or none left while len(stones) > 1: # Sort to get heaviest at the end stones.sort() # Pop two heaviest first = stones.pop() second = stones.pop() # If not equal, push remainder back if first != second: stones.append(first - second) # Return last stone or 0 if none return stones[0] if stones else 0 explanation: | **Time Complexity:** O(n^2 log n) — We sort (O(n log n)) up to n times. **Space Complexity:** O(1) — We modify the input list in-place (or O(n) if counting sort's internal space). This approach sorts the array each iteration to find the two heaviest stones. It's simpler to understand but less efficient. Works fine for the small constraints (n <= 30) but doesn't scale well. The heap approach is preferred for interview settings to demonstrate knowledge of efficient data structures.