questions S-W

This commit is contained in:
2025-05-30 19:18:33 +01:00
parent ddceeec07e
commit 041a877295
46 changed files with 9696 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
title: Stone Game II
slug: stone-game-ii
difficulty: medium
leetcode_id: 1140
leetcode_url: https://leetcode.com/problems/stone-game-ii/
categories:
- arrays
- dynamic-programming
patterns:
- dynamic-programming
- prefix-sum
description: |
Alice and Bob continue their games with piles of stones. There are a number of piles **arranged in a row**, and each pile has a positive integer number of stones `piles[i]`. The objective of the game is to end with the most stones.
Alice and Bob take turns, with Alice starting first.
On each player's turn, that player can take **all the stones** in the **first** `X` remaining piles, where `1 <= X <= 2M`. Then, we set `M = max(M, X)`.
Initially, `M = 1`.
The game continues until all the stones have been taken.
Assuming Alice and Bob play **optimally**, return *the maximum number of stones Alice can get*.
constraints: |
- `1 <= piles.length <= 100`
- `1 <= piles[i] <= 10^4`
examples:
- input: "piles = [2,7,9,4,4]"
output: "10"
explanation: "If Alice takes one pile at the beginning, Bob takes two piles, then Alice takes 2 piles again. Alice can get 2 + 4 + 4 = 10 stones in total. If Alice takes two piles at the beginning, then Bob can take all three piles left. In this case, Alice gets 2 + 7 = 9 stones. So we return 10 since it's larger."
- input: "piles = [1,2,3,4,5,100]"
output: "104"
explanation: "Alice needs to play optimally to secure the pile worth 100 stones."
explanation:
intuition: |
Imagine you and a friend are dividing a row of treasure chests, taking turns from the left side. Each turn, you can take between 1 and `2M` chests, where `M` grows based on how greedy either player gets.
The twist is that **both players play optimally** — they both make the best possible decision at every step. This creates a **minimax** situation: when it's your turn, you want to maximise your score, but you must account for the fact that your opponent will then try to maximise *their* score from the remaining piles.
Think of it like this: at any position in the game, defined by which pile we're starting from (`i`) and the current value of `M`, there's some total number of stones remaining. If you take `X` piles, your opponent then plays optimally from the new state. The key insight is:
**Your score = (Total remaining stones) - (Opponent's optimal score from the next state)**
This works because the stones you *don't* take go to your opponent's potential pool. By using a **suffix sum** (sum of all stones from index `i` to the end), we can quickly calculate the total remaining stones at any point.
We use **memoisation** because the same game state `(i, M)` can be reached through different sequences of moves, and recomputing it each time would be wasteful.
approach: |
We solve this using **Top-Down Dynamic Programming with Memoisation**:
**Step 1: Precompute suffix sums**
- Create a `suffix_sum` array where `suffix_sum[i]` = sum of all stones from index `i` to the end
- This allows O(1) lookup of total remaining stones at any position
- Build it by iterating backwards: `suffix_sum[i] = piles[i] + suffix_sum[i+1]`
&nbsp;
**Step 2: Define the recursive state**
- State: `(i, M)` where `i` is the current pile index and `M` is the current M value
- `dp(i, M)` returns the **maximum stones the current player can get** starting from pile `i` with the given `M`
&nbsp;
**Step 3: Handle base cases**
- If `i >= n` (no piles left), return `0`
- If `i + 2*M >= n` (can take all remaining piles), return `suffix_sum[i]`
&nbsp;
**Step 4: Recursive choice**
- Try taking `X` piles for each valid `X` from `1` to `2*M`
- If we take `X` piles, opponent plays optimally from state `(i + X, max(M, X))`
- Our score = `suffix_sum[i] - dp(i + X, max(M, X))`
- Take the maximum across all choices of `X`
&nbsp;
**Step 5: Return the answer**
- Call `dp(0, 1)` — Alice starts at index 0 with M = 1
common_pitfalls:
- title: Forgetting the Suffix Sum Optimisation
description: |
Without suffix sums, you'd need to sum the remaining piles repeatedly inside the recursion, adding an O(n) factor to each state evaluation.
With `n = 100` and up to `n * n = 10,000` states, this could push the solution towards TLE. Precomputing suffix sums keeps each state evaluation at O(M), which is bounded by O(n).
wrong_approach: "Summing piles[i:] inside each recursive call"
correct_approach: "Precompute suffix_sum array for O(1) remaining sum lookups"
- title: Confusing Whose Score to Maximise
description: |
A common mistake is trying to track Alice's and Bob's scores separately or using different logic for each player's turn.
The elegant insight is that **both players use the same logic**: maximise the current player's score. The relationship `my_score = remaining - opponent_score` handles the adversarial nature automatically.
wrong_approach: "Separate recursion branches for Alice vs Bob"
correct_approach: "Single recursive function that maximises current player's score"
- title: Missing the Greedy Shortcut
description: |
When `i + 2*M >= n`, the current player can take *all* remaining piles. Some implementations miss this base case and continue recursing unnecessarily.
This optimisation also helps with memoisation efficiency — fewer states need to be explored.
wrong_approach: "Always iterating through all X choices even when you can take everything"
correct_approach: "Return suffix_sum[i] immediately when 2*M covers all remaining piles"
- title: Incorrect M Update
description: |
Remember that `M` updates to `max(M, X)`, not just `X`. If the current `M` is already larger than `X`, it stays the same.
For example, if M = 3 and you take X = 2 piles, the new M remains 3, not 2.
wrong_approach: "Setting new_M = X"
correct_approach: "Setting new_M = max(M, X)"
key_takeaways:
- "**Game theory pattern**: In two-player zero-sum games, use `my_score = total - opponent_score` to simplify the logic"
- "**Suffix sum technique**: Precompute cumulative sums from the end when you frequently need 'remaining total' calculations"
- "**State design**: The state `(i, M)` captures everything needed — whose turn it is doesn't matter because both players use identical optimal logic"
- "**Memoisation is essential**: Without caching, the same state would be recomputed exponentially many times"
time_complexity: "O(n^3). We have O(n^2) possible states (index `i` from 0 to n, M from 1 to n), and for each state we try up to O(n) choices of X."
space_complexity: "O(n^2). The memoisation cache stores up to O(n^2) states, plus O(n) for the suffix sum array and recursion stack."
solutions:
- approach_name: Top-Down DP with Memoisation
is_optimal: true
code: |
def stone_game_ii(piles: list[int]) -> int:
n = len(piles)
# Precompute suffix sums for O(1) "remaining stones" lookup
suffix_sum = [0] * (n + 1)
for i in range(n - 1, -1, -1):
suffix_sum[i] = piles[i] + suffix_sum[i + 1]
# Memoisation cache: (index, M) -> max stones for current player
memo = {}
def dp(i: int, m: int) -> int:
# Base case: no piles left
if i >= n:
return 0
# Optimisation: can take all remaining piles
if i + 2 * m >= n:
return suffix_sum[i]
# Check cache
if (i, m) in memo:
return memo[(i, m)]
# Try all valid choices of X (1 to 2M piles)
max_stones = 0
for x in range(1, 2 * m + 1):
# Remaining after we take X piles
remaining = suffix_sum[i]
# Opponent's optimal score from new state
opponent_score = dp(i + x, max(m, x))
# Our score = remaining - what opponent gets
our_score = remaining - opponent_score
max_stones = max(max_stones, our_score)
memo[(i, m)] = max_stones
return max_stones
# Alice starts at index 0 with M = 1
return dp(0, 1)
explanation: |
**Time Complexity:** O(n^3) — O(n^2) states, each examining up to O(n) choices.
**Space Complexity:** O(n^2) — Memoisation cache plus suffix sum array.
The key insight is using suffix sums to quickly calculate remaining stones, and the relationship `my_score = remaining - opponent_score` to handle the adversarial game elegantly. Memoisation ensures each state is computed only once.
- approach_name: Bottom-Up DP
is_optimal: true
code: |
def stone_game_ii(piles: list[int]) -> int:
n = len(piles)
# Precompute suffix sums
suffix_sum = [0] * (n + 1)
for i in range(n - 1, -1, -1):
suffix_sum[i] = piles[i] + suffix_sum[i + 1]
# dp[i][m] = max stones current player can get starting at i with M = m
# M can be at most n (taking all piles at once)
dp = [[0] * (n + 1) for _ in range(n + 1)]
# Fill table backwards (from end of piles to start)
for i in range(n - 1, -1, -1):
for m in range(1, n + 1):
# Can take all remaining piles
if i + 2 * m >= n:
dp[i][m] = suffix_sum[i]
else:
# Try all valid X choices
max_stones = 0
for x in range(1, 2 * m + 1):
opponent_score = dp[i + x][max(m, x)]
our_score = suffix_sum[i] - opponent_score
max_stones = max(max_stones, our_score)
dp[i][m] = max_stones
return dp[0][1]
explanation: |
**Time Complexity:** O(n^3) — Same as top-down approach.
**Space Complexity:** O(n^2) — 2D DP table plus suffix sum array.
This iterative version fills the DP table from the end backwards. It's functionally equivalent to the memoised version but may have slightly better constant factors due to avoiding recursion overhead. The logic remains the same: maximise current player's stones using the suffix sum relationship.