Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Base Case: If i >= n (where n is the length of stoneValue), there are no more stones to take, so return 0.

  2. 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 (where j ranges from 0 to 2):
      • Add stoneValue[i + j] to our running sum s
      • Calculate the score difference as s - dfs(i + j + 1)
      • Update ans with the maximum value
  3. 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 Evaluator

Example 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 gets dfs(4) = 0
    • Score difference: -9 - 0 = -9
  • 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): gain 3, opponent gets dfs(3) = -9
    • Score difference: 3 - (-9) = 12
  • Take 2 stones (values 3, -9 = -6): gain -6, opponent gets dfs(4) = 0
    • Score difference: -6 - 0 = -6
  • 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): gain 2, opponent gets dfs(2) = 12
    • Score difference: 2 - 12 = -10
  • Take 2 stones (values 2, 3 = 5): gain 5, opponent gets dfs(3) = -9
    • Score difference: 5 - (-9) = 14
  • Take 3 stones (values 2, 3, -9 = -4): gain -4, opponent gets dfs(4) = 0
    • Score difference: -4 - 0 = -4
  • Maximum: 14

dfs(0): Starting at index 0, all stones [1, 2, 3, -9] available

  • Take 1 stone (value 1): gain 1, opponent gets dfs(1) = 14
    • Score difference: 1 - 14 = -13
  • Take 2 stones (values 1, 2 = 3): gain 3, opponent gets dfs(2) = 12
    • Score difference: 3 - 12 = -9
  • Take 3 stones (values 1, 2, 3 = 6): gain 6, opponent gets dfs(3) = -9
    • Score difference: 6 - (-9) = 15
  • 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:

  1. Memoization cache: The @cache decorator stores the results of dfs(i) for each unique value of i from 0 to n-1, requiring O(n) space.
  2. Recursion call stack: In the worst case, the recursion depth can go up to n levels (when taking one stone at each step), requiring O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More