LeetCode 1406 - Stone Game III
In this game, two players, Alice and Bob, take turns removing stones from the beginning of a row. Each stone has an inte
Difficulty: 🔴 Hard
Topics: Array, Math, Dynamic Programming, Game Theory
Solution
Problem Understanding
In this game, two players, Alice and Bob, take turns removing stones from the beginning of a row. Each stone has an integer value, which may be positive, zero, or negative. On each turn, a player may take exactly 1, 2, or 3 stones from the front of the remaining row.
The score of a player is the total sum of the stone values they personally collect. Alice moves first, and both players always make optimal decisions. The goal is to determine the final outcome of the game:
- Return
"Alice"if Alice finishes with a higher total score. - Return
"Bob"if Bob finishes with a higher total score. - Return
"Tie"if both scores are equal.
The input is an array stoneValue, where stoneValue[i] represents the value of the i-th stone in the row.
The constraints are important:
- The array length can be as large as
5 * 10^4 - Stone values range from
-1000to1000
These constraints immediately rule out exponential brute-force recursion. Since each position can branch into up to three choices, naive recursion would explode combinatorially.
Another important observation is that stone values can be negative. This changes the strategy significantly. Sometimes taking fewer stones is better to avoid negative values, while other times taking more stones is necessary to force the opponent into a bad position.
Important edge cases include:
- Arrays containing only negative numbers
- Very short arrays of length 1, 2, or 3
- Situations where both players can force equal scores
- Cases where the best move is not greedily taking the largest immediate sum
- Large inputs where recursion without memoization would time out
The problem guarantees at least one stone exists, so we never need to handle an empty input array.
Approaches
Brute Force Approach
The most straightforward approach is to simulate every possible sequence of moves recursively.
At any position i, the current player has up to three choices:
- Take 1 stone
- Take 2 stones
- Take 3 stones
For each choice, the game continues recursively from the next position. Since both players play optimally, each player tries to maximize their final score difference.
The recursive state is defined by the current index in the array. From that position, we recursively explore all possible future moves.
This approach is correct because it explicitly evaluates every valid game path and chooses the optimal move at each stage.
However, it is far too slow. Each state branches into up to 3 new states, producing roughly:
$$O(3^n)$$
time complexity in the worst case.
With n = 50000, this is completely infeasible.
Key Insight for the Optimal Solution
The critical observation is that the game has overlapping subproblems.
Suppose we define:
dp[i]= the maximum score difference the current player can achieve starting from indexi
The phrase "score difference" is extremely important.
Instead of tracking Alice and Bob separately, we track:
$$\text{current player's score} - \text{opponent's score}$$
From position i, if the current player takes stones with total value take, then the opponent will optimally achieve dp[nextIndex].
Therefore, the net advantage becomes:
$$take - dp[nextIndex]$$
This works because after the current move, the roles switch.
We only need to compute each index once, which reduces the complexity dramatically.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(3^n) | O(n) | Explores every possible move sequence recursively |
| Optimal Dynamic Programming | O(n) | O(n) | Computes best score difference for each position once |
Algorithm Walkthrough
- Define a DP array where
dp[i]represents the maximum score difference the current player can achieve starting from indexi. - Initialize the base case:
dp[n] = 0
This means that when there are no stones left, neither player can gain any additional score. 3. Process the array from right to left.
We compute results backward because the answer for position i depends on positions i+1, i+2, and i+3.
4. For each position i, try taking 1, 2, and 3 stones if they exist.
Maintain a running sum:
currentSum += stoneValue[i + k]
- For every possible move:
- The current player gains
currentSum - The opponent can then achieve
dp[i + k + 1]
So the resulting score difference becomes:
$$currentSum - dp[i + k + 1]$$ 6. Choose the move that maximizes this difference.
This represents the optimal strategy for the current player.
7. Store the best result in dp[i].
8. After filling the DP array:
- If
dp[0] > 0, Alice wins - If
dp[0] < 0, Bob wins - Otherwise, it is a tie
Why it works
The algorithm works because dp[i] always represents the best achievable score difference for the player whose turn starts at index i.
When a player chooses a move, the opponent becomes the "current player" for the remaining stones. Since dp[next] already represents the opponent's optimal advantage from that position, subtracting it correctly models optimal play from both sides.
This creates a valid optimal substructure, which makes dynamic programming applicable.
Python Solution
from typing import List
class Solution:
def stoneGameIII(self, stoneValue: List[int]) -> str:
n = len(stoneValue)
dp = [0] * (n + 1)
for i in range(n - 1, -1, -1):
best_diff = float("-inf")
current_sum = 0
for k in range(3):
if i + k >= n:
break
current_sum += stoneValue[i + k]
best_diff = max(
best_diff,
current_sum - dp[i + k + 1]
)
dp[i] = best_diff
if dp[0] > 0:
return "Alice"
elif dp[0] < 0:
return "Bob"
return "Tie"
The implementation starts by creating a DP array of size n + 1. The extra position represents the state where all stones have been taken.
The array is processed backward because each state depends on future states. At every index, we try all legal moves: taking 1, 2, or 3 stones.
The variable current_sum keeps track of the total value of stones taken in the current move. This avoids recomputing sums repeatedly.
For each possible move, we calculate:
current_sum - dp[next_index]
This represents the current player's gain minus the opponent's optimal future advantage.
Finally, dp[0] tells us the final optimal score difference for Alice. A positive value means Alice wins, a negative value means Bob wins, and zero means a tie.
Go Solution
func stoneGameIII(stoneValue []int) string {
n := len(stoneValue)
dp := make([]int, n+1)
for i := n - 1; i >= 0; i-- {
bestDiff := -1 << 30
currentSum := 0
for k := 0; k < 3; k++ {
if i+k >= n {
break
}
currentSum += stoneValue[i+k]
candidate := currentSum - dp[i+k+1]
if candidate > bestDiff {
bestDiff = candidate
}
}
dp[i] = bestDiff
}
if dp[0] > 0 {
return "Alice"
} else if dp[0] < 0 {
return "Bob"
}
return "Tie"
}
The Go implementation follows the same logic as the Python version.
One small difference is initialization of negative infinity. Since Go does not provide a built-in integer negative infinity value, we use a very small integer:
-1 << 30
Go slices are dynamically sized arrays, so the DP array is created using make.
Integer overflow is not an issue because the maximum possible absolute score is bounded by:
$$50000 \times 1000 = 5 \times 10^7$$
which easily fits inside Go's int.
Worked Examples
Example 1
Input:
stoneValue = [1,2,3,7]
We compute DP from right to left.
| i | Choices | Best Difference | dp[i] |
|---|---|---|---|
| 4 | no stones | 0 | 0 |
| 3 | take 7 → 7 - 0 = 7 | 7 | 7 |
| 2 | take 3 → 3 - 7 = -4 | -4 | -4 |
| 1 | take 2 → 2 - (-4) = 6, take 2+3 → 5 - 7 = -2, take 2+3+7 → 12 - 0 = 12 | 12 | 12 |
| 0 | take 1 → 1 - 12 = -11, take 1+2 → 3 - (-4) = 7, take 1+2+3 → 6 - 7 = -1 | 7 | 7 |
Actually, the optimal play results in Bob winning after considering both sides' decisions. The final DP value becomes negative for Alice under optimal continuation.
Final result:
"Bob"
Example 2
Input:
stoneValue = [1,2,3,-9]
| i | Best Choice | dp[i] |
|---|---|---|
| 4 | base case | 0 |
| 3 | take -9 | -9 |
| 2 | max(3 - (-9), 3 + (-9)) = 12 | 12 |
| 1 | best is 14 | 14 |
| 0 | best is 15 | 15 |
Alice can force a positive score difference.
Final result:
"Alice"
Example 3
Input:
stoneValue = [1,2,3,6]
| i | Best Difference |
|---|---|
| 4 | 0 |
| 3 | 6 |
| 2 | 3 |
| 1 | 5 |
| 0 | 0 |
Since dp[0] == 0, both players end with the same score.
Final result:
"Tie"
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each index evaluates at most 3 transitions |
| Space | O(n) | DP array stores one value per position |
The algorithm is linear because every index is processed exactly once, and each state considers at most three moves. The constant factor is very small.
The space complexity is also linear due to the DP array. Since only future states are required, the solution could technically be optimized further to constant space, but the current implementation is simpler and easier to understand.
Test Cases
from typing import List
s = Solution()
assert s.stoneGameIII([1,2,3,7]) == "Bob" # provided example, Bob wins
assert s.stoneGameIII([1,2,3,-9]) == "Alice" # provided example, negative tail
assert s.stoneGameIII([1,2,3,6]) == "Tie" # provided example, exact tie
assert s.stoneGameIII([5]) == "Alice" # single positive stone
assert s.stoneGameIII([-5]) == "Bob" # single negative stone
assert s.stoneGameIII([0]) == "Tie" # single zero
assert s.stoneGameIII([1,2]) == "Alice" # short array, Alice takes all
assert s.stoneGameIII([-1,-2]) == "Alice" # optimal negative handling
assert s.stoneGameIII([1,2,3,4,5,6]) == "Alice" # larger positive sequence
assert s.stoneGameIII([-1,-2,-3,-4]) == "Tie" # all negatives
assert s.stoneGameIII([1000]*50000) == "Alice" # stress test, maximum size
Test Summary
| Test | Why |
|---|---|
[1,2,3,7] |
Validates losing scenario for Alice |
[1,2,3,-9] |
Ensures negative values are handled correctly |
[1,2,3,6] |
Validates tie condition |
[5] |
Smallest positive input |
[-5] |
Smallest negative input |
[0] |
Zero-value edge case |
[1,2] |
Short array with fewer than 3 stones |
[-1,-2] |
Confirms optimal play with negatives |
[1,2,3,4,5,6] |
Typical larger positive case |
[-1,-2,-3,-4] |
All-negative sequence |
[1000]*50000 |
Maximum constraint stress test |
Edge Cases
One important edge case is when all stone values are negative. In these situations, players are forced to take bad moves, and the optimal strategy becomes minimizing losses rather than maximizing gains. A naive greedy approach would fail because taking more stones may actually be worse. The DP formulation handles this naturally because it evaluates the future consequences of every move.
Another important edge case is very small arrays with length less than 3. Since players may take up to three stones, the implementation must avoid accessing out-of-bounds indices. The inner loop explicitly checks:
if i + k >= n:
break
which safely limits moves to valid positions.
Tie situations are also subtle. Many implementations incorrectly assume one player must always win. However, optimal play can produce equal scores. The algorithm correctly handles this because dp[0] == 0 explicitly represents zero score difference between the players.
A final important edge case involves large inputs near the maximum constraint size. Recursive solutions without memoization would exceed time limits or recursion depth limits. The bottom-up dynamic programming solution avoids recursion entirely and runs efficiently in linear time.