Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1406. Stone Game III - Leetcode Solution

Problem Description

In the Stone Game III problem, you are given an array stoneValue where each element represents the value of a stone in a row. Two players, Alice and Bob, take turns playing. On each turn, a player can take the first 1, 2, or 3 stones from the row (i.e., from the start of the remaining stones). The player who takes the stones adds their values to their own score, and the stones are removed from the row.

Both players play optimally, meaning they try to maximize their own score. The game continues until all stones are taken. At the end, the player with the higher score wins. If both scores are equal, the result is a tie.

  • Return "Alice" if Alice wins, "Bob" if Bob wins, or "Tie" if their scores are equal.
  • Constraints: 1 <= stoneValue.length <= 5 * 10^4, -1000 <= stoneValue[i] <= 1000

Thought Process

At first glance, this problem seems to require simulating every possible sequence of moves for both players, as each can choose to take 1, 2, or 3 stones on their turn. However, this brute-force approach quickly becomes infeasible due to the exponential number of possible move sequences.

To optimize, we recognize that the game is about maximizing the difference between the current player's score and the opponent's, assuming both play optimally. This is a classic case for dynamic programming: we can define the problem recursively in terms of the best possible outcome from any given starting point, and use memoization or tabulation to avoid recalculating states.

The key insight is to work backwards: for each position in the array, we can calculate the best net score difference the current player can achieve, considering all possible moves (taking 1, 2, or 3 stones).

Solution Approach

To solve the problem efficiently, we use dynamic programming. Here’s a step-by-step breakdown:

  1. Define State: Let dp[i] represent the maximum net score difference the current player can achieve starting from index i in stoneValue. A positive value means the current player is ahead; negative means behind.
  2. Base Case: If i is at or beyond the end of the array, there are no stones left, so dp[i] = 0.
  3. Transition: For each i, try taking 1, 2, or 3 stones (if available). For each choice:
    • Sum the values of the stones taken.
    • The opponent will play next, starting at i + x (where x is the number of stones taken).
    • The net score is: current_sum - dp[i + x] (because the opponent gets to play optimally from the new position).
    Take the maximum of these possible net scores.
  4. Final Answer: After filling dp, look at dp[0]:
    • If dp[0] > 0, Alice wins.
    • If dp[0] < 0, Bob wins.
    • If dp[0] == 0, it's a tie.
  5. Optimization: Instead of recursion + memoization, we can use bottom-up DP, iterating from the end of the array backwards and storing results in a 1D array.

Example Walkthrough

Let's walk through an example with stoneValue = [1,2,3,7]:

  • Step 1: Start from the end. For i = 3 (stoneValue[3] = 7), Alice can only take one stone:
    • Take 1: get 7, opponent gets nothing. So dp[3] = 7.
  • Step 2: For i = 2 (stoneValue[2] = 3):
    • Take 1: get 3, opponent can get dp[3] = 7, so net = 3 - 7 = -4.
    • Take 2: get 3+7=10, opponent gets nothing, net = 10 - 0 = 10.
    • Take 3: not possible (out of bounds).
    • So, dp[2] = max(-4, 10) = 10.
  • Step 3: For i = 1 (stoneValue[1] = 2):
    • Take 1: get 2, opponent gets dp[2] = 10, net = 2 - 10 = -8.
    • Take 2: get 2+3=5, opponent gets dp[3] = 7, net = 5 - 7 = -2.
    • Take 3: get 2+3+7=12, opponent gets nothing, net = 12 - 0 = 12.
    • So, dp[1] = max(-8, -2, 12) = 12.
  • Step 4: For i = 0 (stoneValue[0] = 1):
    • Take 1: get 1, opponent gets dp[1] = 12, net = 1 - 12 = -11.
    • Take 2: get 1+2=3, opponent gets dp[2] = 10, net = 3 - 10 = -7.
    • Take 3: get 1+2+3=6, opponent gets dp[3] = 7, net = 6 - 7 = -1.
    • So, dp[0] = max(-11, -7, -1) = -1.
  • Since dp[0] = -1, Alice is behind by 1 point, so Bob wins.

Time and Space Complexity

  • Brute-force: Exponential time, since each move can branch into up to 3 choices recursively for every stone.
  • Optimized DP:
    • Time Complexity: O(n), as we process each index once and for each index, only consider up to 3 options.
    • Space Complexity: O(n) for the DP array. (Can be reduced to O(1) with rolling variables, but O(n) is standard and clear.)

Summary

The Stone Game III problem is a classic example of applying dynamic programming to a two-player, turn-based game with optimal strategies. By modeling the game as a series of state transitions and calculating the maximum net score difference at each step, we efficiently determine the winner. The elegance of the solution lies in reducing an exponential search space to a linear-time dynamic programming approach, making it both efficient and conceptually clear.

Code Implementation

class Solution:
    def stoneGameIII(self, stoneValue: List[int]) -> str:
        n = len(stoneValue)
        dp = [0] * (n + 1)
        for i in range(n - 1, -1, -1):
            max_diff = float('-inf')
            take = 0
            for k in range(1, 4):
                if i + k - 1 < n:
                    take += stoneValue[i + k - 1]
                    max_diff = max(max_diff, take - dp[i + k])
            dp[i] = max_diff
        if dp[0] > 0:
            return "Alice"
        elif dp[0] < 0:
            return "Bob"
        else:
            return "Tie"
    
class Solution {
public:
    string stoneGameIII(vector<int>& stoneValue) {
        int n = stoneValue.size();
        vector<int> dp(n + 1, 0);
        for (int i = n - 1; i >= 0; --i) {
            int maxDiff = INT_MIN, take = 0;
            for (int k = 1; k <= 3; ++k) {
                if (i + k - 1 < n) {
                    take += stoneValue[i + k - 1];
                    maxDiff = max(maxDiff, take - dp[i + k]);
                }
            }
            dp[i] = maxDiff;
        }
        if (dp[0] > 0) return "Alice";
        else if (dp[0] < 0) return "Bob";
        else return "Tie";
    }
};
    
class Solution {
    public String stoneGameIII(int[] stoneValue) {
        int n = stoneValue.length;
        int[] dp = new int[n + 1];
        for (int i = n - 1; i >= 0; --i) {
            int maxDiff = Integer.MIN_VALUE, take = 0;
            for (int k = 1; k <= 3; ++k) {
                if (i + k - 1 < n) {
                    take += stoneValue[i + k - 1];
                    maxDiff = Math.max(maxDiff, take - dp[i + k]);
                }
            }
            dp[i] = maxDiff;
        }
        if (dp[0] > 0) return "Alice";
        else if (dp[0] < 0) return "Bob";
        else return "Tie";
    }
}
    
var stoneGameIII = function(stoneValue) {
    const n = stoneValue.length;
    const dp = new Array(n + 1).fill(0);
    for (let i = n - 1; i >= 0; --i) {
        let maxDiff = -Infinity, take = 0;
        for (let k = 1; k <= 3; ++k) {
            if (i + k - 1 < n) {
                take += stoneValue[i + k - 1];
                maxDiff = Math.max(maxDiff, take - dp[i + k]);
            }
        }
        dp[i] = maxDiff;
    }
    if (dp[0] > 0) return "Alice";
    else if (dp[0] < 0) return "Bob";
    else return "Tie";
};