questions S-W
This commit is contained in:
258
backend/data/questions/stone-game-iii.yaml
Normal file
258
backend/data/questions/stone-game-iii.yaml
Normal file
@@ -0,0 +1,258 @@
|
||||
title: Stone Game III
|
||||
slug: stone-game-iii
|
||||
difficulty: hard
|
||||
leetcode_id: 1406
|
||||
leetcode_url: https://leetcode.com/problems/stone-game-iii/
|
||||
categories:
|
||||
- arrays
|
||||
- dynamic-programming
|
||||
patterns:
|
||||
- dynamic-programming
|
||||
|
||||
description: |
|
||||
Alice and Bob continue their games with piles of stones. There are several stones **arranged in a row**, and each stone has an associated value which is an integer given in the array `stoneValue`.
|
||||
|
||||
Alice and Bob take turns, with Alice starting first. On each player's turn, that player can take `1`, `2`, or `3` stones from the **first** remaining stones in the row.
|
||||
|
||||
The score of each player is the sum of the values of the stones taken. The score of each player is `0` initially.
|
||||
|
||||
The objective of the game is to end with the highest score, and the winner is the player with the highest score and there could be a tie. The game continues until all the stones have been taken.
|
||||
|
||||
Assume Alice and Bob **play optimally**.
|
||||
|
||||
Return `"Alice"` *if Alice will win*, `"Bob"` *if Bob will win*, or `"Tie"` *if they will end the game with the same score*.
|
||||
|
||||
constraints: |
|
||||
- `1 <= stoneValue.length <= 5 * 10^4`
|
||||
- `-1000 <= stoneValue[i] <= 1000`
|
||||
|
||||
examples:
|
||||
- input: "stoneValue = [1,2,3,7]"
|
||||
output: '"Bob"'
|
||||
explanation: "Alice will always lose. Her best move will be to take three piles and the score becomes 6. Now the score of Bob is 7 and Bob wins."
|
||||
- input: "stoneValue = [1,2,3,-9]"
|
||||
output: '"Alice"'
|
||||
explanation: "Alice must choose all the three piles at the first move to win and leave Bob with negative score. If Alice chooses one pile her score will be 1 and the next move Bob's score becomes 5. In the next move, Alice will take the pile with value = -9 and lose."
|
||||
- input: "stoneValue = [1,2,3,6]"
|
||||
output: '"Tie"'
|
||||
explanation: "Alice cannot win this game. She can end the game in a draw if she decided to choose all the first three piles, otherwise she will lose."
|
||||
|
||||
explanation:
|
||||
intuition: |
|
||||
Imagine you're at a buffet line where each dish has a "value" — some positive (delicious) and some negative (terrible). You and your opponent take turns, and each turn you must take 1, 2, or 3 consecutive dishes from the front of the line. Both of you want to maximize your own total value.
|
||||
|
||||
The key insight is the **zero-sum nature** of this game: whatever stones remain after you pick, your opponent will play optimally on those remaining stones. So instead of tracking both players' scores separately, we can think in terms of **relative advantage**.
|
||||
|
||||
Define `dp[i]` as the maximum **score difference** (current player's score minus opponent's score) that the current player can achieve starting from index `i`. When it's your turn at position `i`:
|
||||
|
||||
- If you take stones `i` to `i+k-1` (where `k` is 1, 2, or 3), you gain those values
|
||||
- Then your opponent plays optimally from position `i+k`, achieving `dp[i+k]` for themselves
|
||||
- Your relative advantage becomes: `sum of stones taken - dp[i+k]`
|
||||
|
||||
The subtraction of `dp[i+k]` captures the **minimax** principle — your opponent's best outcome becomes your deficit.
|
||||
|
||||
At the end, if `dp[0] > 0`, Alice (who starts) has a positive advantage and wins. If `dp[0] < 0`, Bob wins. If `dp[0] == 0`, it's a tie.
|
||||
|
||||
approach: |
|
||||
We solve this using **Dynamic Programming** with state representing the relative score difference:
|
||||
|
||||
**Step 1: Define the DP state**
|
||||
|
||||
- `dp[i]`: Maximum score difference (current player minus opponent) achievable starting from index `i`
|
||||
- Base case: `dp[n] = 0` (no stones left means no advantage)
|
||||
|
||||
|
||||
|
||||
**Step 2: Build the recurrence relation**
|
||||
|
||||
- At each position `i`, the current player can take 1, 2, or 3 stones
|
||||
- For each choice `k` (1, 2, or 3):
|
||||
- Player gains: `stoneValue[i] + stoneValue[i+1] + ... + stoneValue[i+k-1]`
|
||||
- Opponent then achieves: `dp[i+k]` from the remaining stones
|
||||
- Net advantage: `sum of k stones - dp[i+k]`
|
||||
- Choose the maximum among all valid options
|
||||
|
||||
|
||||
|
||||
**Step 3: Iterate backwards from the end**
|
||||
|
||||
- Process positions from `n-1` down to `0`
|
||||
- Use a suffix sum to efficiently calculate the sum of stones taken
|
||||
- Track `dp[i+1]`, `dp[i+2]`, `dp[i+3]` for the three possible moves
|
||||
|
||||
|
||||
|
||||
**Step 4: Determine the winner**
|
||||
|
||||
- If `dp[0] > 0`: Alice wins (she has positive advantage)
|
||||
- If `dp[0] < 0`: Bob wins (Alice has negative advantage, meaning Bob is ahead)
|
||||
- If `dp[0] == 0`: Tie
|
||||
|
||||
common_pitfalls:
|
||||
- title: Tracking Both Scores Separately
|
||||
description: |
|
||||
A natural first instinct is to track Alice's score and Bob's score as separate states. This leads to a 2D DP with states like `dp[i][aliceScore][bobScore]`, which has prohibitive complexity.
|
||||
|
||||
The insight is that we only care about the **difference** between scores, not the absolute values. This reduces the problem to a single dimension: the relative advantage of whoever is currently playing.
|
||||
wrong_approach: "Track separate scores for Alice and Bob"
|
||||
correct_approach: "Track score difference (current player - opponent)"
|
||||
|
||||
- title: Forgetting Negative Stone Values
|
||||
description: |
|
||||
Unlike simpler stone game variants, this problem has **negative values**. This means sometimes the optimal play is to take fewer stones to force your opponent to take negative ones.
|
||||
|
||||
For example, with `[1, 2, 3, -9]`, Alice's optimal move is to take all three positive stones (sum = 6) and leave Bob with just the -9, giving Bob a score of -9. Alice wins 6 to -9.
|
||||
|
||||
If Alice only took one stone, Bob could take `[2, 3]` (sum = 5) and leave Alice with -9. Bob would win.
|
||||
wrong_approach: "Assume taking more stones is always better"
|
||||
correct_approach: "Consider all 1, 2, 3 stone options and pick the best difference"
|
||||
|
||||
- title: Off-by-One Errors in Suffix Sums
|
||||
description: |
|
||||
When calculating the sum of the next `k` stones, be careful with indices. The sum from index `i` taking `k` stones is `suffixSum[i] - suffixSum[i+k]`, not `suffixSum[i+k]`.
|
||||
|
||||
Also ensure you handle the case where `i + k > n` by treating out-of-bounds `dp` values as 0.
|
||||
wrong_approach: "Incorrect suffix sum indexing"
|
||||
correct_approach: "Use suffixSum[i] - suffixSum[min(i+k, n)] for sum of k stones"
|
||||
|
||||
key_takeaways:
|
||||
- "**Minimax principle**: In two-player zero-sum games, maximizing your advantage equals minimizing your opponent's advantage"
|
||||
- "**Score difference DP**: Track relative advantage instead of absolute scores to reduce state complexity"
|
||||
- "**Backward iteration**: Process from end to start, as each state depends on future states"
|
||||
- "**Foundation for game theory**: This pattern applies to many competitive game problems (Stone Game variants, Nim, etc.)"
|
||||
|
||||
time_complexity: "O(n). We compute each `dp[i]` exactly once, and each computation considers at most 3 previous states."
|
||||
space_complexity: "O(n) for the DP array. Can be optimized to O(1) since we only need the last 3 DP values."
|
||||
|
||||
solutions:
|
||||
- approach_name: Dynamic Programming (Score Difference)
|
||||
is_optimal: true
|
||||
code: |
|
||||
def stone_game_iii(stone_value: list[int]) -> str:
|
||||
n = len(stone_value)
|
||||
|
||||
# dp[i] = max score difference (current player - opponent)
|
||||
# starting from index i
|
||||
# We need dp[i+1], dp[i+2], dp[i+3], so use array of size n+1
|
||||
dp = [0] * (n + 1)
|
||||
|
||||
# Suffix sum for efficient range sum calculation
|
||||
suffix_sum = [0] * (n + 1)
|
||||
for i in range(n - 1, -1, -1):
|
||||
suffix_sum[i] = suffix_sum[i + 1] + stone_value[i]
|
||||
|
||||
# Fill DP from right to left
|
||||
for i in range(n - 1, -1, -1):
|
||||
# Try taking 1, 2, or 3 stones
|
||||
# Initialize to negative infinity to find maximum
|
||||
dp[i] = float('-inf')
|
||||
|
||||
for k in range(1, 4): # k = 1, 2, or 3 stones
|
||||
if i + k <= n:
|
||||
# Sum of stones taken: suffix_sum[i] - suffix_sum[i+k]
|
||||
stones_taken = suffix_sum[i] - suffix_sum[i + k]
|
||||
# Our advantage = stones we get - opponent's best outcome
|
||||
dp[i] = max(dp[i], stones_taken - dp[i + k])
|
||||
|
||||
# Determine winner based on Alice's advantage (she starts at index 0)
|
||||
if dp[0] > 0:
|
||||
return "Alice"
|
||||
elif dp[0] < 0:
|
||||
return "Bob"
|
||||
else:
|
||||
return "Tie"
|
||||
explanation: |
|
||||
**Time Complexity:** O(n) — Single pass through the array from right to left, with O(1) work per position.
|
||||
|
||||
**Space Complexity:** O(n) — For the DP array and suffix sum array.
|
||||
|
||||
We define `dp[i]` as the maximum score difference achievable by the current player starting from index `i`. By iterating backwards and considering all three choices (take 1, 2, or 3 stones), we build up the optimal strategy. The minimax principle is captured by subtracting `dp[i+k]` — the opponent's best outcome becomes our deficit.
|
||||
|
||||
- approach_name: Space-Optimized DP
|
||||
is_optimal: true
|
||||
code: |
|
||||
def stone_game_iii(stone_value: list[int]) -> str:
|
||||
n = len(stone_value)
|
||||
|
||||
# Only need last 3 DP values
|
||||
# dp_next[0] = dp[i+1], dp_next[1] = dp[i+2], dp_next[2] = dp[i+3]
|
||||
dp_next = [0, 0, 0]
|
||||
|
||||
# Process from right to left
|
||||
suffix = 0 # Running suffix sum starting from position i
|
||||
|
||||
for i in range(n - 1, -1, -1):
|
||||
suffix += stone_value[i]
|
||||
|
||||
# Try taking 1, 2, or 3 stones
|
||||
best = float('-inf')
|
||||
take_sum = 0
|
||||
|
||||
for k in range(1, 4):
|
||||
if i + k - 1 < n:
|
||||
take_sum += stone_value[i + k - 1]
|
||||
# Opponent's best is dp_next[k-1] (which is dp[i+k])
|
||||
opponent_best = dp_next[k - 1] if k <= 3 else 0
|
||||
best = max(best, take_sum - opponent_best)
|
||||
|
||||
# Shift the window: dp[i+3] <- dp[i+2] <- dp[i+1] <- dp[i]
|
||||
dp_next[2] = dp_next[1]
|
||||
dp_next[1] = dp_next[0]
|
||||
dp_next[0] = best
|
||||
|
||||
# dp_next[0] now holds dp[0], Alice's maximum advantage
|
||||
if dp_next[0] > 0:
|
||||
return "Alice"
|
||||
elif dp_next[0] < 0:
|
||||
return "Bob"
|
||||
else:
|
||||
return "Tie"
|
||||
explanation: |
|
||||
**Time Complexity:** O(n) — Single pass through the array.
|
||||
|
||||
**Space Complexity:** O(1) — Only stores 3 previous DP values.
|
||||
|
||||
This optimized version recognizes that `dp[i]` only depends on `dp[i+1]`, `dp[i+2]`, and `dp[i+3]`. By maintaining a sliding window of just 3 values, we reduce space from O(n) to O(1) while preserving the same logic.
|
||||
|
||||
- approach_name: Recursive with Memoization
|
||||
is_optimal: false
|
||||
code: |
|
||||
def stone_game_iii(stone_value: list[int]) -> str:
|
||||
n = len(stone_value)
|
||||
memo = {}
|
||||
|
||||
def dp(i: int) -> int:
|
||||
"""Return max score difference for current player starting at i."""
|
||||
if i >= n:
|
||||
return 0
|
||||
|
||||
if i in memo:
|
||||
return memo[i]
|
||||
|
||||
# Try taking 1, 2, or 3 stones
|
||||
best = float('-inf')
|
||||
take_sum = 0
|
||||
|
||||
for k in range(1, 4):
|
||||
if i + k - 1 < n:
|
||||
take_sum += stone_value[i + k - 1]
|
||||
# Our advantage = stones taken - opponent's best
|
||||
best = max(best, take_sum - dp(i + k))
|
||||
|
||||
memo[i] = best
|
||||
return best
|
||||
|
||||
result = dp(0)
|
||||
|
||||
if result > 0:
|
||||
return "Alice"
|
||||
elif result < 0:
|
||||
return "Bob"
|
||||
else:
|
||||
return "Tie"
|
||||
explanation: |
|
||||
**Time Complexity:** O(n) — Each state computed once due to memoization.
|
||||
|
||||
**Space Complexity:** O(n) — For memoization dictionary and recursion stack.
|
||||
|
||||
This top-down approach may be more intuitive for some. We recursively compute the best score difference from each position, caching results to avoid recomputation. The logic is identical to the bottom-up DP but expressed recursively.
|
||||
Reference in New Issue
Block a user