1406. Stone Game III
Problem Description
Alice and Bob are playing a stone game with a row of stones, where each stone has an integer value given in the array stoneValue
.
The game follows these rules:
- Alice and Bob take turns, with Alice going first
- On each turn, a player must take 1, 2, or 3 stones from the beginning of the remaining row
- Each player's score is the sum of values of all stones they've taken
- Both players start with a score of 0
- The game continues until all stones are taken
- Both players play optimally (making the best possible moves)
The goal is to determine who wins when both players play perfectly. Return:
"Alice"
if Alice wins (has a higher score)"Bob"
if Bob wins (has a higher score)"Tie"
if both players end with the same score
For example, if stoneValue = [1, 2, 3, 7]
:
- Alice could take 1 stone (value 1), then Bob might take 3 stones (values 2, 3, 7 = 12)
- Or Alice could take 2 stones (values 1, 2 = 3), leaving Bob with different options
- The key is that both players will choose the strategy that maximizes their own score
The challenge is to determine the outcome when both players make optimal decisions at every turn.
Intuition
When both players play optimally, each player tries to maximize their own advantage. The key insight is that we can think about this game in terms of score difference rather than absolute scores.
Since players alternate turns, when it's my turn, I want to maximize the difference between my score and my opponent's score. Whatever stones I take add to my score, and then my opponent will play optimally on their turn to maximize their advantage.
This leads us to think recursively: at any position i
in the array, the current player needs to decide whether to take 1, 2, or 3 stones. If they take j+1
stones (where j
can be 0, 1, or 2), they gain the sum of those stone values. But then the opponent gets to play from position i+j+1
, and the opponent will also play optimally.
The crucial realization is that if we define dfs(i)
as the maximum score difference the current player can achieve starting from position i
, then:
- When I take some stones and gain their values, that's positive for my score difference
- When my opponent plays next, they'll achieve their best score difference from the remaining stones, which is negative for my score difference
So if I take j+1
stones starting from position i
, my net score difference is:
(sum of stones I take) - (opponent's best score difference from remaining stones)
This can be written as: sum(stoneValue[i] to stoneValue[i+j]) - dfs(i+j+1)
Since I want to maximize my advantage, I should choose the option (taking 1, 2, or 3 stones) that gives me the maximum score difference.
At the very beginning (i=0
), Alice plays first. If dfs(0) > 0
, it means Alice can achieve a positive score difference (she wins). If dfs(0) < 0
, Bob wins. If dfs(0) = 0
, it's a tie.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution implements a recursive approach with memoization to find the optimal outcome.
Implementation Details:
We define a recursive function dfs(i)
that calculates the maximum score difference the current player can achieve when starting from index i
:
-
Base Case: If
i >= n
(wheren
is the length ofstoneValue
), there are no more stones to take, so return0
. -
Recursive Case: For each valid move (taking 1, 2, or 3 stones), we:
- Initialize
ans = -inf
to track the maximum score difference - Initialize
s = 0
to accumulate the sum of stones being taken - Try taking
j+1
stones (wherej
ranges from 0 to 2):- Add
stoneValue[i + j]
to our running sums
- Calculate the score difference as
s - dfs(i + j + 1)
- Update
ans
with the maximum value
- Add
- Initialize
-
Memoization: The
@cache
decorator is used to store previously computed results, preventing redundant calculations for the same state.
The formula for the recursive relation is:
dfs(i) = max(sum(stoneValue[i:i+j+1]) - dfs(i+j+1)) for j in {0, 1, 2}
Final Result Determination:
After computing dfs(0)
, which represents the maximum score difference Alice can achieve as the first player:
- If
dfs(0) > 0
: Alice wins (return "Alice") - If
dfs(0) < 0
: Bob wins (return "Bob") - If
dfs(0) == 0
: It's a tie (return "Tie")
Time Complexity: O(n)
where n
is the number of stones, since each state i
is computed at most once due to memoization.
Space Complexity: O(n)
for the memoization cache and recursion stack.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with stoneValue = [1, 2, 3, -9]
to illustrate how the solution works.
We'll trace through the recursive function dfs(i)
which returns the maximum score difference the current player can achieve starting from index i
.
Starting from the end (building up our memoization):
dfs(4)
: Base case - no stones left, returns 0
dfs(3)
: Starting at index 3, only stone value -9
remains
- Take 1 stone: gain
-9
, opponent getsdfs(4) = 0
- Score difference:
-9 - 0 = -9
- Score difference:
- Can't take 2 or 3 stones (out of bounds)
- Maximum:
-9
dfs(2)
: Starting at index 2, stones [3, -9]
remain
- Take 1 stone (value
3
): gain3
, opponent getsdfs(3) = -9
- Score difference:
3 - (-9) = 12
- Score difference:
- Take 2 stones (values
3, -9 = -6
): gain-6
, opponent getsdfs(4) = 0
- Score difference:
-6 - 0 = -6
- Score difference:
- Can't take 3 stones (out of bounds)
- Maximum:
12
dfs(1)
: Starting at index 1, stones [2, 3, -9]
remain
- Take 1 stone (value
2
): gain2
, opponent getsdfs(2) = 12
- Score difference:
2 - 12 = -10
- Score difference:
- Take 2 stones (values
2, 3 = 5
): gain5
, opponent getsdfs(3) = -9
- Score difference:
5 - (-9) = 14
- Score difference:
- Take 3 stones (values
2, 3, -9 = -4
): gain-4
, opponent getsdfs(4) = 0
- Score difference:
-4 - 0 = -4
- Score difference:
- Maximum:
14
dfs(0)
: Starting at index 0, all stones [1, 2, 3, -9]
available
- Take 1 stone (value
1
): gain1
, opponent getsdfs(1) = 14
- Score difference:
1 - 14 = -13
- Score difference:
- Take 2 stones (values
1, 2 = 3
): gain3
, opponent getsdfs(2) = 12
- Score difference:
3 - 12 = -9
- Score difference:
- Take 3 stones (values
1, 2, 3 = 6
): gain6
, opponent getsdfs(3) = -9
- Score difference:
6 - (-9) = 15
- Score difference:
- Maximum:
15
Result: dfs(0) = 15 > 0
, so Alice wins!
This means when both players play optimally, Alice can achieve a score that's 15 points higher than Bob's. Alice's optimal strategy is to take the first 3 stones (gaining 6 points), forcing Bob to take the last stone with value -9.
Solution Implementation
1class Solution:
2 def stoneGameIII(self, stoneValue: List[int]) -> str:
3 from functools import cache
4 from math import inf
5
6 @cache
7 def dfs(index: int) -> int:
8 """
9 Calculate the maximum score difference the current player can achieve
10 starting from index position.
11
12 Args:
13 index: Current position in the stone array
14
15 Returns:
16 Maximum score difference (current player's score - opponent's score)
17 """
18 # Base case: no stones left
19 if index >= n:
20 return 0
21
22 # Try taking 1, 2, or 3 stones and find the best outcome
23 max_score_diff = -inf
24 current_sum = 0
25
26 for num_stones in range(3):
27 # Check if we can take this many stones
28 if index + num_stones >= n:
29 break
30
31 # Add the stone value to current sum
32 current_sum += stoneValue[index + num_stones]
33
34 # Calculate score difference:
35 # current_sum (what we take) - dfs(next) (opponent's best from remaining)
36 score_diff = current_sum - dfs(index + num_stones + 1)
37 max_score_diff = max(max_score_diff, score_diff)
38
39 return max_score_diff
40
41 # Initialize game parameters
42 n = len(stoneValue)
43
44 # Get Alice's maximum score difference
45 alice_score_diff = dfs(0)
46
47 # Determine winner based on score difference
48 if alice_score_diff == 0:
49 return 'Tie'
50 elif alice_score_diff > 0:
51 return 'Alice'
52 else:
53 return 'Bob'
54
1class Solution {
2 // Cache for memoization to store computed results
3 private int[] stoneValue;
4 private Integer[] memo;
5 private int n;
6
7 /**
8 * Determines the winner of the stone game.
9 * Alice and Bob take turns, with Alice going first.
10 * Each player can take 1, 2, or 3 stones from the beginning of the array.
11 * Both players play optimally to maximize their score difference.
12 *
13 * @param stoneValue Array of stone values
14 * @return "Alice" if Alice wins, "Bob" if Bob wins, "Tie" if it's a draw
15 */
16 public String stoneGameIII(int[] stoneValue) {
17 // Initialize instance variables
18 this.n = stoneValue.length;
19 this.memo = new Integer[n];
20 this.stoneValue = stoneValue;
21
22 // Calculate the maximum score difference Alice can achieve
23 int scoreDifference = dfs(0);
24
25 // Determine the winner based on score difference
26 if (scoreDifference == 0) {
27 return "Tie";
28 }
29 return scoreDifference > 0 ? "Alice" : "Bob";
30 }
31
32 /**
33 * Recursively calculates the maximum score difference the current player
34 * can achieve starting from index i.
35 *
36 * The score difference is: (current player's score) - (opponent's score)
37 *
38 * @param i Starting index for the current turn
39 * @return Maximum score difference achievable from position i
40 */
41 private int dfs(int i) {
42 // Base case: no stones left to take
43 if (i >= n) {
44 return 0;
45 }
46
47 // Return cached result if already computed
48 if (memo[i] != null) {
49 return memo[i];
50 }
51
52 // Initialize with minimum possible value
53 int maxScoreDifference = Integer.MIN_VALUE;
54 int currentSum = 0;
55
56 // Try taking 1, 2, or 3 stones
57 for (int j = 0; j < 3 && i + j < n; j++) {
58 // Add the value of the current stone to our sum
59 currentSum += stoneValue[i + j];
60
61 // Calculate score difference:
62 // Our score (currentSum) minus the best the opponent can do from the next position
63 maxScoreDifference = Math.max(maxScoreDifference, currentSum - dfs(i + j + 1));
64 }
65
66 // Cache and return the result
67 memo[i] = maxScoreDifference;
68 return maxScoreDifference;
69 }
70}
71
1class Solution {
2public:
3 string stoneGameIII(vector<int>& stoneValue) {
4 int n = stoneValue.size();
5
6 // dp[i] represents the maximum score difference the current player can achieve
7 // starting from index i (current player's score - opponent's score)
8 vector<int> dp(n, INT_MAX);
9
10 // Recursive function with memoization to calculate the maximum score difference
11 function<int(int)> dfs = [&](int index) -> int {
12 // Base case: no stones left to take
13 if (index >= n) {
14 return 0;
15 }
16
17 // Return memoized result if already calculated
18 if (dp[index] != INT_MAX) {
19 return dp[index];
20 }
21
22 // Initialize the maximum score difference to negative infinity
23 int maxScoreDiff = INT_MIN;
24 int currentSum = 0;
25
26 // Try taking 1, 2, or 3 stones from current position
27 for (int take = 0; take < 3 && index + take < n; ++take) {
28 // Add the stone value to current sum
29 currentSum += stoneValue[index + take];
30
31 // Calculate score difference:
32 // current sum - opponent's best score from next position
33 // The opponent's score becomes negative because we're maximizing our difference
34 maxScoreDiff = max(maxScoreDiff, currentSum - dfs(index + take + 1));
35 }
36
37 // Memoize and return the result
38 dp[index] = maxScoreDiff;
39 return dp[index];
40 };
41
42 // Calculate the score difference when Alice starts first
43 int scoreDifference = dfs(0);
44
45 // Determine the winner based on score difference
46 if (scoreDifference == 0) {
47 return "Tie";
48 }
49 return scoreDifference > 0 ? "Alice" : "Bob";
50 }
51};
52
1/**
2 * Determines the winner of Stone Game III using dynamic programming with memoization.
3 * Alice and Bob take turns removing stones, Alice goes first.
4 * Each player can take 1, 2, or 3 stones from the beginning of the array.
5 * The player with the highest total stone value wins.
6 *
7 * @param stoneValue - Array of stone values
8 * @returns "Alice" if Alice wins, "Bob" if Bob wins, or "Tie" if it's a draw
9 */
10function stoneGameIII(stoneValue: number[]): string {
11 const n: number = stoneValue.length;
12 const INFINITY: number = 1 << 30; // Large value representing uncomputed state
13
14 // Memoization array to store the maximum score difference the current player can achieve
15 // starting from index i
16 const memo: number[] = new Array(n).fill(INFINITY);
17
18 /**
19 * Recursive function with memoization to calculate the maximum score difference
20 * the current player can achieve starting from index i.
21 *
22 * @param index - Current position in the stone array
23 * @returns Maximum score difference the current player can achieve
24 */
25 const calculateMaxScoreDifference = (index: number): number => {
26 // Base case: no stones left to take
27 if (index >= n) {
28 return 0;
29 }
30
31 // Return memoized result if already computed
32 if (memo[index] !== INFINITY) {
33 return memo[index];
34 }
35
36 let maxScoreDifference: number = -INFINITY;
37 let currentSum: number = 0;
38
39 // Try taking 1, 2, or 3 stones
40 for (let stonesCount: number = 0; stonesCount < 3 && index + stonesCount < n; stonesCount++) {
41 // Add current stone to the sum
42 currentSum += stoneValue[index + stonesCount];
43
44 // Calculate score difference: current player's gain minus opponent's best outcome
45 // from the remaining stones
46 const scoreDifference: number = currentSum - calculateMaxScoreDifference(index + stonesCount + 1);
47 maxScoreDifference = Math.max(maxScoreDifference, scoreDifference);
48 }
49
50 // Store and return the result
51 memo[index] = maxScoreDifference;
52 return maxScoreDifference;
53 };
54
55 // Calculate the final score difference from Alice's perspective
56 const aliceScoreDifference: number = calculateMaxScoreDifference(0);
57
58 // Determine the winner based on the score difference
59 if (aliceScoreDifference === 0) {
60 return 'Tie';
61 }
62 return aliceScoreDifference > 0 ? 'Alice' : 'Bob';
63}
64
Time and Space Complexity
Time Complexity: O(n)
The algorithm uses memoization with the @cache
decorator. The dfs
function is called with parameter i
ranging from 0
to n-1
. Due to memoization, each unique state dfs(i)
is computed only once. For each state, we iterate at most 3 times (constant time) to consider taking 1, 2, or 3 stones. Therefore, we have n
unique states, each taking O(1)
time to compute, resulting in a total time complexity of O(n)
.
Space Complexity: O(n)
The space complexity consists of two components:
- Memoization cache: The
@cache
decorator stores the results ofdfs(i)
for each unique value ofi
from0
ton-1
, requiringO(n)
space. - Recursion call stack: In the worst case, the recursion depth can go up to
n
levels (when taking one stone at each step), requiringO(n)
space for the call stack.
Both components contribute O(n)
space, so the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Score Calculation - Adding Opponent's Score Instead of Subtracting
A common mistake is incorrectly calculating the score difference by adding the opponent's best score instead of subtracting it:
Incorrect Implementation:
# WRONG: Adding opponent's score score_diff = current_sum + dfs(index + num_stones + 1)
Why it's wrong: The recursive function returns the score difference from the current player's perspective. When we move to the next state, the opponent becomes the current player. So we need to subtract their best outcome from our current gain.
Correct Implementation:
# CORRECT: Subtracting opponent's score score_diff = current_sum - dfs(index + num_stones + 1)
2. Off-by-One Error in Loop Range
Another frequent mistake is using incorrect bounds when trying to take stones:
Incorrect Implementation:
# WRONG: Loop goes from 1 to 3 instead of 0 to 2
for num_stones in range(1, 4):
if index + num_stones > n: # Also wrong boundary check
break
current_sum += stoneValue[index + num_stones - 1]
score_diff = current_sum - dfs(index + num_stones)
Why it's wrong:
- Using
range(1, 4)
with direct indexing creates confusion and potential index errors - The boundary check
index + num_stones > n
is off by one
Correct Implementation:
# CORRECT: Loop from 0 to 2, representing taking 1 to 3 stones
for num_stones in range(3):
if index + num_stones >= n:
break
current_sum += stoneValue[index + num_stones]
score_diff = current_sum - dfs(index + num_stones + 1)
3. Forgetting to Accumulate Stone Values
A subtle but critical error is recalculating the sum each iteration instead of accumulating:
Incorrect Implementation:
for num_stones in range(3):
if index + num_stones >= n:
break
# WRONG: Only considering single stone value, not cumulative
stone_value = stoneValue[index + num_stones]
score_diff = stone_value - dfs(index + num_stones + 1)
max_score_diff = max(max_score_diff, score_diff)
Why it's wrong: When taking 2 or 3 stones, we need the sum of all stones taken, not just the last one.
Correct Implementation:
current_sum = 0
for num_stones in range(3):
if index + num_stones >= n:
break
# CORRECT: Accumulating all stone values
current_sum += stoneValue[index + num_stones]
score_diff = current_sum - dfs(index + num_stones + 1)
max_score_diff = max(max_score_diff, score_diff)
4. Using Global Variables Without Proper Initialization
When using memoization with @cache
, avoid relying on global variables that might change between test cases:
Incorrect Implementation:
class Solution:
def stoneGameIII(self, stoneValue: List[int]) -> str:
n = len(stoneValue)
@cache
def dfs(index):
# WRONG: Accessing stoneValue directly from outer scope
# This can cause issues if the function is reused
if index >= len(stoneValue):
return 0
# ... rest of the code
Correct Implementation:
class Solution:
def stoneGameIII(self, stoneValue: List[int]) -> str:
n = len(stoneValue)
@cache
def dfs(index):
# CORRECT: Using pre-computed n variable
if index >= n:
return 0
# Access stoneValue through consistent scope
Best Practice: Clear the cache between different test cases if needed, or ensure the memoization key uniquely identifies the problem state.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!