questions F-L

This commit is contained in:
2025-05-25 11:47:04 +01:00
parent ecf95bd23d
commit 917c371529
54 changed files with 11235 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
title: Last Stone Weight II
slug: last-stone-weight-ii
difficulty: medium
leetcode_id: 1049
leetcode_url: https://leetcode.com/problems/last-stone-weight-ii/
categories:
- dynamic-programming
- arrays
patterns:
- dynamic-programming
description: |
You are given an array of integers `stones` where `stones[i]` is the weight of the i<sup>th</sup> stone.
We are playing a game with the stones. On each turn, we choose any two stones and smash them together. Suppose the 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 smallest possible weight of the left stone*. If there are no stones left, return `0`.
constraints: |
- `1 <= stones.length <= 30`
- `1 <= stones[i] <= 100`
examples:
- input: "stones = [2,7,4,1,8,1]"
output: "1"
explanation: "We can combine 2 and 4 to get 2, so the array converts to [2,7,1,8,1] then, we can combine 7 and 8 to get 1, so the array converts to [2,1,1,1] then, we can combine 2 and 1 to get 1, so the array converts to [1,1,1] then, we can combine 1 and 1 to get 0, so the array converts to [1], then that's the optimal value."
- input: "stones = [31,26,33,21,40]"
output: "5"
explanation: "One way is to smash 31 and 33 to get 2, then smash 26 and 21 to get 5, then smash 40 and 5 to get 35, then smash 35 and 2 to get 33, then smash 33 and 5 to get 28... Actually, the minimum achievable is 5 by optimal partitioning."
explanation:
intuition: |
At first glance, this looks like a simulation problem — keep smashing stones until one remains. But simulating every possible order of smashing would be exponentially complex. There must be a deeper insight.
Here's the key realisation: **smashing is equivalent to assigning signs**. When we smash stones `x` and `y`, we get `|y - x|`. If we keep smashing the results, we're essentially computing `|(a - b) - c|` = `|a - b - c|` or similar expressions. In the end, each original stone contributes either positively or negatively to the final result.
Think of it like this: imagine labelling each stone with `+` or `-`. The final stone's weight equals `|sum of stones with + signs| - |sum of stones with - signs|`. We want to minimise this difference.
This transforms the problem into: **partition the stones into two groups such that the absolute difference between their sums is minimised**. This is the classic "minimum subset sum difference" problem, which is a variant of the 0/1 knapsack.
If the total sum is `S`, and one group has sum `subset_sum`, the other has sum `S - subset_sum`. The difference is `|S - 2 * subset_sum|`. To minimise this, we want `subset_sum` as close to `S / 2` as possible.
approach: |
We solve this using **0/1 Knapsack DP** to find the closest achievable sum to half the total:
**Step 1: Calculate the target**
- Compute `total = sum(stones)`
- Our goal: find the largest `subset_sum <= total // 2` that we can form
- The answer will be `total - 2 * subset_sum`
&nbsp;
**Step 2: Create the DP set**
- Use a set `dp` to track all achievable sums
- Initialise with `{0}` — we can always achieve sum 0 (empty subset)
&nbsp;
**Step 3: Process each stone**
- For each stone, we can either include it or not (0/1 knapsack)
- For each existing sum `s` in our set, `s + stone` is now also achievable
- Add these new sums to our set (but only up to `total // 2` to save space)
&nbsp;
**Step 4: Find the best partition**
- The largest value in `dp` that doesn't exceed `total // 2` is our best `subset_sum`
- Return `total - 2 * subset_sum`
&nbsp;
The set-based approach is elegant and efficient for this problem's constraints. With `stones.length <= 30` and `stones[i] <= 100`, the maximum total is 3000, making this approach very practical.
common_pitfalls:
- title: Trying to Simulate Smashing
description: |
The naive approach of simulating all possible smashing orders has exponential complexity. With 30 stones, there are far too many orderings to try.
The key insight is recognising this as a partitioning problem, not a simulation problem. Once you see that smashing assigns implicit +/- signs, the path to DP becomes clear.
wrong_approach: "Recursively try all pairs of stones to smash"
correct_approach: "Reduce to minimum partition difference using DP"
- title: Using Unbounded Knapsack
description: |
Unlike Coin Change where each coin can be used infinitely, here each stone can only be used **once**. This is 0/1 knapsack, not unbounded knapsack.
If you iterate incorrectly, you might count the same stone multiple times, leading to wrong answers.
wrong_approach: "for stone in stones: for s in dp: add s + stone"
correct_approach: "Process stones one at a time, updating dp carefully"
- title: Forgetting to Limit the Target
description: |
Since we want subset_sum closest to `total / 2`, we only need to track sums up to `total // 2`. Tracking larger sums is redundant — if one group has sum `> total / 2`, the other has sum `< total / 2`, and we'd already have that smaller sum.
This optimisation keeps memory usage reasonable.
wrong_approach: "Track all possible sums up to total"
correct_approach: "Only track sums up to total // 2"
key_takeaways:
- "**Problem reduction**: Recognising that smashing stones = partitioning with +/- signs transforms an intractable simulation into a classic DP problem"
- "**0/1 Knapsack pattern**: Each stone can be used at most once — this is the defining characteristic of 0/1 knapsack"
- "**Minimum partition difference**: Finding two subsets with minimum sum difference is equivalent to finding one subset closest to half the total"
- "**Set-based DP**: Using a set to track achievable sums is clean and efficient for moderate constraints"
time_complexity: "O(n × S) where n is the number of stones and S is the total sum. For each stone, we potentially add up to S/2 new sums."
space_complexity: "O(S) where S is the total sum. The set stores at most S/2 + 1 achievable sums."
solutions:
- approach_name: Set-Based DP
is_optimal: true
code: |
def last_stone_weight_ii(stones: list[int]) -> int:
total = sum(stones)
target = total // 2
# dp stores all achievable subset sums
dp = {0}
for stone in stones:
# For each existing sum, we can add this stone
# Create new set to avoid modifying during iteration
new_sums = set()
for s in dp:
if s + stone <= target:
new_sums.add(s + stone)
dp.update(new_sums)
# Find largest achievable sum <= target
best_sum = max(dp)
# Difference between two groups: (total - best_sum) - best_sum
return total - 2 * best_sum
explanation: |
**Time Complexity:** O(n × S) — For each of n stones, we iterate through sums up to S/2.
**Space Complexity:** O(S) — Set stores achievable sums up to S/2.
We track all achievable subset sums using a set. For each stone, we compute new achievable sums by adding it to existing sums. The largest sum we can achieve that doesn't exceed half the total gives us the closest partition to equal, minimising the leftover stone weight.
- approach_name: Boolean Array DP
is_optimal: true
code: |
def last_stone_weight_ii(stones: list[int]) -> int:
total = sum(stones)
target = total // 2
# dp[i] = True if sum i is achievable
dp = [False] * (target + 1)
dp[0] = True # Empty subset has sum 0
for stone in stones:
# Iterate backwards to avoid using same stone twice
for s in range(target, stone - 1, -1):
if dp[s - stone]:
dp[s] = True
# Find largest achievable sum
for s in range(target, -1, -1):
if dp[s]:
return total - 2 * s
return total # Fallback (shouldn't reach here)
explanation: |
**Time Complexity:** O(n × S) — Same as set-based approach.
**Space Complexity:** O(S) — Boolean array of size S/2 + 1.
This uses a classic 0/1 knapsack boolean array. The key is iterating **backwards** when updating — this ensures each stone is only counted once per subset. If we iterated forwards, we'd potentially add the same stone multiple times.
- approach_name: Brute Force (Exponential)
is_optimal: false
code: |
def last_stone_weight_ii(stones: list[int]) -> int:
def find_min_diff(index: int, sum1: int, sum2: int) -> int:
# Base case: all stones assigned
if index == len(stones):
return abs(sum1 - sum2)
# Try putting current stone in group 1 or group 2
put_in_group1 = find_min_diff(index + 1, sum1 + stones[index], sum2)
put_in_group2 = find_min_diff(index + 1, sum1, sum2 + stones[index])
return min(put_in_group1, put_in_group2)
return find_min_diff(0, 0, 0)
explanation: |
**Time Complexity:** O(2^n) — Each stone has 2 choices, giving 2^n subsets.
**Space Complexity:** O(n) — Recursion stack depth.
This brute force tries all possible partitions by assigning each stone to either group. While correct, it's far too slow for n=30 (over a billion combinations). Included to illustrate the problem structure and why DP is necessary.