225 lines
10 KiB
YAML
225 lines
10 KiB
YAML
title: Stone Game
|
|
slug: stone-game
|
|
difficulty: medium
|
|
leetcode_id: 877
|
|
leetcode_url: https://leetcode.com/problems/stone-game/
|
|
categories:
|
|
- arrays
|
|
- dynamic-programming
|
|
- math
|
|
patterns:
|
|
- dynamic-programming
|
|
|
|
function_signature: "def stone_game(piles: list[int]) -> bool:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { piles: [5, 3, 4, 5] }
|
|
expected: true
|
|
- input: { piles: [3, 7, 2, 3] }
|
|
expected: true
|
|
hidden:
|
|
- input: { piles: [1, 2] }
|
|
expected: true
|
|
- input: { piles: [1, 100, 1, 100] }
|
|
expected: true
|
|
- input: { piles: [7, 8, 8, 10] }
|
|
expected: true
|
|
- input: { piles: [3, 2, 10, 4] }
|
|
expected: true
|
|
|
|
description: |
|
|
Alice and Bob play a game with piles of stones. There are an **even** 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. The **total** number of stones across all the piles is **odd**, so there are no ties.
|
|
|
|
Alice and Bob take turns, with **Alice starting first**. Each turn, a player takes the entire pile of stones either from the **beginning** or from the **end** of the row. This continues until there are no more piles left, at which point the person with the **most stones wins**.
|
|
|
|
Assuming Alice and Bob play optimally, return `true` *if Alice wins the game, or* `false` *if Bob wins*.
|
|
|
|
constraints: |
|
|
- `2 <= piles.length <= 500`
|
|
- `piles.length` is **even**
|
|
- `1 <= piles[i] <= 500`
|
|
- `sum(piles[i])` is **odd**
|
|
|
|
examples:
|
|
- input: "piles = [5,3,4,5]"
|
|
output: "true"
|
|
explanation: "Alice starts first, and can only take the first 5 or the last 5. Say she takes the first 5, so that the row becomes [3, 4, 5]. If Bob takes 3, then the board is [4, 5], and Alice takes 5 to win with 10 points. If Bob takes the last 5, then the board is [3, 4], and Alice takes 4 to win with 9 points. Taking the first 5 was a winning move for Alice."
|
|
- input: "piles = [3,7,2,3]"
|
|
output: "true"
|
|
explanation: "Alice can always win by playing optimally."
|
|
|
|
explanation:
|
|
intuition: |
|
|
This is a classic **two-player game theory** problem where both players play optimally. At first glance, it seems like we need complex dynamic programming to simulate all possible game states.
|
|
|
|
However, there's a beautiful mathematical insight: **Alice can always win**. Here's why:
|
|
|
|
Think of the piles as being at positions `0, 1, 2, ..., n-1`. Since `n` is even, we can separate them into:
|
|
- **Even-indexed piles**: positions `0, 2, 4, ...`
|
|
- **Odd-indexed piles**: positions `1, 3, 5, ...`
|
|
|
|
The key insight is that **Alice can always choose to take all even-indexed piles OR all odd-indexed piles**. Here's how:
|
|
- If Alice wants even-indexed piles, she starts by taking `piles[0]` (the first pile)
|
|
- This forces Bob to choose between positions `1` and `n-1` — both odd-indexed!
|
|
- Whatever Bob picks, Alice's next choice will again include an even-indexed pile
|
|
- Alice continues this pattern, always having access to even-indexed positions
|
|
|
|
Since the total sum is odd and there are no ties, either the even-indexed sum or the odd-indexed sum must be larger. Alice simply chooses the strategy that gives her the larger total. Therefore, **Alice always wins**.
|
|
|
|
approach: |
|
|
We present two approaches: the elegant mathematical solution and the DP solution for educational purposes.
|
|
|
|
**Mathematical Approach (Optimal)**
|
|
|
|
**Step 1: Recognise the game structure**
|
|
|
|
- With an even number of piles, Alice can control whether she collects all even-indexed or all odd-indexed piles
|
|
- The sums of these two groups are different (total is odd, so no ties)
|
|
|
|
|
|
|
|
**Step 2: Conclude Alice wins**
|
|
|
|
- Alice computes which group (even or odd indices) has more stones
|
|
- She plays the strategy to collect that group
|
|
- Return `True` unconditionally
|
|
|
|
|
|
|
|
**Dynamic Programming Approach (Educational)**
|
|
|
|
**Step 1: Define the state**
|
|
|
|
- `dp[i][j]`: The maximum *advantage* (difference in stones) the current player can achieve over their opponent when choosing from `piles[i..j]`
|
|
|
|
|
|
|
|
**Step 2: Base case**
|
|
|
|
- `dp[i][i] = piles[i]`: With one pile, the current player takes it all (advantage = pile value)
|
|
|
|
|
|
|
|
**Step 3: Transition**
|
|
|
|
- Current player can take `piles[i]` (left) or `piles[j]` (right)
|
|
- If they take `piles[i]`, opponent then plays optimally on `piles[i+1..j]`
|
|
- The opponent's advantage becomes our disadvantage: `dp[i][j] = piles[i] - dp[i+1][j]`
|
|
- Similarly for taking right: `dp[i][j] = piles[j] - dp[i][j-1]`
|
|
- Take the maximum of both choices
|
|
|
|
|
|
|
|
**Step 4: Return result**
|
|
|
|
- `dp[0][n-1] > 0` means Alice has positive advantage, so she wins
|
|
|
|
common_pitfalls:
|
|
- title: Overcomplicating with Full Simulation
|
|
description: |
|
|
Many people immediately jump to complex game-tree simulation or minimax with alpha-beta pruning. While these work, they're overkill for this problem.
|
|
|
|
The mathematical insight that Alice can always control parity makes the solution trivial. Always look for structural properties before implementing complex algorithms.
|
|
wrong_approach: "Full game tree exploration"
|
|
correct_approach: "Recognise Alice controls even/odd parity"
|
|
|
|
- title: Missing the Parity Control Insight
|
|
description: |
|
|
It's not obvious that the first player can force a specific subset of piles. The key is realising that after Alice takes from one end, Bob is forced to take from an odd-indexed position (relative to the remaining array), and this pattern continues.
|
|
|
|
With an even-length array, this control over parity is absolute and deterministic.
|
|
wrong_approach: "Assuming both players have equal opportunity"
|
|
correct_approach: "Alice dictates the parity pattern"
|
|
|
|
- title: Incorrect DP State Definition
|
|
description: |
|
|
A common mistake is defining `dp[i][j]` as "the maximum stones the current player can get" rather than "the maximum advantage over the opponent."
|
|
|
|
Using advantage simplifies the recurrence because taking a pile and then having the opponent play optimally means your advantage is `pile_value - opponent's_advantage`.
|
|
wrong_approach: "dp[i][j] = max stones for current player"
|
|
correct_approach: "dp[i][j] = max advantage (stone difference) for current player"
|
|
|
|
key_takeaways:
|
|
- "**Look for structural insights**: Before implementing complex algorithms, check if the problem has mathematical properties that simplify it drastically"
|
|
- "**Parity arguments in games**: When array length is even, the first player often has control over which subset of elements they can guarantee"
|
|
- "**Minimax with advantage**: In two-player zero-sum games, defining DP state as 'advantage over opponent' often simplifies the recurrence"
|
|
- "**Foundation for game theory**: This problem introduces concepts used in Stone Game II, III, and other game theory problems"
|
|
|
|
time_complexity: "O(1) for the mathematical solution (Alice always wins). O(n^2) for the DP solution where `n` is the number of piles."
|
|
space_complexity: "O(1) for the mathematical solution. O(n^2) for the DP solution to store the 2D table."
|
|
|
|
solutions:
|
|
- approach_name: Mathematical (Parity Argument)
|
|
is_optimal: true
|
|
code: |
|
|
def stone_game(piles: list[int]) -> bool:
|
|
# Alice can always choose to take all even-indexed
|
|
# or all odd-indexed piles. Since total is odd,
|
|
# one group must be larger. Alice picks that strategy.
|
|
# Therefore, Alice always wins.
|
|
return True
|
|
explanation: |
|
|
**Time Complexity:** O(1) — No computation needed.
|
|
|
|
**Space Complexity:** O(1) — No extra space used.
|
|
|
|
This elegant solution leverages the structural property that Alice, moving first with an even number of piles, can always force a winning strategy by controlling which parity of indices she collects.
|
|
|
|
- approach_name: Dynamic Programming
|
|
is_optimal: false
|
|
code: |
|
|
def stone_game(piles: list[int]) -> bool:
|
|
n = len(piles)
|
|
# dp[i][j] = max advantage current player can achieve on piles[i..j]
|
|
dp = [[0] * n for _ in range(n)]
|
|
|
|
# Base case: single pile, take it all
|
|
for i in range(n):
|
|
dp[i][i] = piles[i]
|
|
|
|
# Fill for increasing lengths
|
|
for length in range(2, n + 1):
|
|
for i in range(n - length + 1):
|
|
j = i + length - 1
|
|
# Take left pile: gain piles[i], opponent plays on [i+1, j]
|
|
take_left = piles[i] - dp[i + 1][j]
|
|
# Take right pile: gain piles[j], opponent plays on [i, j-1]
|
|
take_right = piles[j] - dp[i][j - 1]
|
|
# Choose the better option
|
|
dp[i][j] = max(take_left, take_right)
|
|
|
|
# If Alice's advantage is positive, she wins
|
|
return dp[0][n - 1] > 0
|
|
explanation: |
|
|
**Time Complexity:** O(n^2) — We fill an n x n DP table.
|
|
|
|
**Space Complexity:** O(n^2) — Storage for the 2D DP table.
|
|
|
|
This approach explicitly computes the optimal play using interval DP. While it correctly solves the problem, the mathematical solution is simpler. This DP approach is valuable for understanding game theory and extends to variants like Stone Game II and III where the mathematical shortcut doesn't apply.
|
|
|
|
- approach_name: Space-Optimised DP
|
|
is_optimal: false
|
|
code: |
|
|
def stone_game(piles: list[int]) -> bool:
|
|
n = len(piles)
|
|
# dp[i] represents the advantage for interval starting at i
|
|
# We only need the previous row to compute the current row
|
|
dp = piles[:] # Base case: dp[i] = piles[i] for length 1
|
|
|
|
for length in range(2, n + 1):
|
|
for i in range(n - length + 1):
|
|
j = i + length - 1
|
|
# Take left or right, subtract opponent's optimal play
|
|
dp[i] = max(piles[i] - dp[i + 1], piles[j] - dp[i])
|
|
|
|
return dp[0] > 0
|
|
explanation: |
|
|
**Time Complexity:** O(n^2) — Same iteration as 2D DP.
|
|
|
|
**Space Complexity:** O(n) — Only a 1D array needed.
|
|
|
|
This optimises the 2D DP by observing that we only need the previous diagonal to compute the current one. We reuse a 1D array, updating it in-place. This is useful when `n` is large and O(n^2) space becomes a concern.
|