Facebook Pixel

486. Predict the Winner

Problem Description

You have an integer array nums and two players are playing a game with it. The game works as follows:

  • Player 1 and Player 2 take turns, with Player 1 going first
  • Both players start with a score of 0
  • On each turn, a player must take one number from either end of the array (either nums[0] or nums[nums.length - 1])
  • After taking a number, the array shrinks by 1 element
  • The player adds the taken number to their score
  • The game continues until no elements remain in the array

Your task is to determine if Player 1 can win the game. Player 1 wins if their final score is greater than or equal to Player 2's score (ties count as wins for Player 1). You should return true if Player 1 can win, and false otherwise.

Both players play optimally, meaning they always make the best possible move to maximize their own chance of winning.

For example, if nums = [1, 5, 2]:

  • Player 1 could take 2 (from the right), leaving [1, 5]
  • Player 2 would then take 5 (from the right), leaving [1]
  • Player 1 would take 1
  • Final scores: Player 1 = 3, Player 2 = 5, so Player 1 loses

But if Player 1 plays optimally:

  • Player 1 takes 1 (from the left), leaving [5, 2]
  • Player 2 takes 5 (from the left), leaving [2]
  • Player 1 takes 2
  • Final scores: Player 1 = 3, Player 2 = 5, so Player 1 still loses

However, with optimal play from the start:

  • Player 1 takes 2 (from the right), leaving [1, 5]
  • Player 2 must take either 1 or 5
  • The optimal strategy ensures the best outcome for each player
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to think about the game from the perspective of score differences rather than absolute scores. When a player makes a move, they're not just trying to maximize their own score - they're trying to maximize the difference between their score and their opponent's score.

Let's think about what happens when it's your turn with array segment from index i to j. You have two choices:

  • Take nums[i] from the left
  • Take nums[j] from the right

If you take nums[i], you gain nums[i] points. But then it becomes your opponent's turn with the remaining array from i+1 to j. Your opponent will play optimally and try to maximize their advantage over you. So the net advantage you get is: nums[i] minus whatever advantage your opponent gets from the remaining array.

Similarly, if you take nums[j], your net advantage is: nums[j] minus whatever advantage your opponent gets from array i to j-1.

This naturally leads to a recursive structure where we define dfs(i, j) as the maximum score difference (current player's score minus opponent's score) that the current player can achieve when playing optimally on the subarray from index i to j.

The beautiful part is that this captures the alternating nature of the game. When we compute dfs(i, j), we're looking at the current player's advantage. When we recursively call dfs(i+1, j) or dfs(i, j-1), that represents the opponent's advantage in their turn, which we need to subtract from our gain.

At the end, if dfs(0, n-1) >= 0, it means Player 1 can achieve a score difference of at least 0, meaning they can win or tie (which counts as a win).

The memoization with @cache is crucial because the same subarray might be evaluated multiple times through different game paths, and we don't want to recalculate the same optimal play repeatedly.

Solution Approach

We implement the solution using dynamic programming with memoization. The core function dfs(i, j) represents the maximum score difference that the current player can achieve when playing optimally on the subarray from index i to j.

Base Case: When i > j, there are no numbers left to pick, so the score difference is 0. This happens when all elements have been taken.

Recursive Case: For a valid range [i, j], the current player has two choices:

  1. Take the left element nums[i]:

    • The player gains nums[i] points
    • The opponent then plays optimally on [i+1, j] and achieves a maximum difference of dfs(i+1, j)
    • The net difference for the current player is: nums[i] - dfs(i+1, j)
  2. Take the right element nums[j]:

    • The player gains nums[j] points
    • The opponent then plays optimally on [i, j-1] and achieves a maximum difference of dfs(i, j-1)
    • The net difference for the current player is: nums[j] - dfs(i, j-1)

The current player chooses the option that maximizes their advantage:

dfs(i, j) = max(nums[i] - dfs(i+1, j), nums[j] - dfs(i, j-1))

Memoization: The @cache decorator automatically memoizes the function results. This prevents redundant calculations when the same subarray [i, j] is encountered multiple times through different game sequences. Without memoization, the time complexity would be exponential; with it, we achieve polynomial time.

Final Answer: We call dfs(0, len(nums) - 1) to get Player 1's maximum achievable score difference for the entire array. If this value is >= 0, Player 1 can win or tie (both count as wins), so we return True. Otherwise, we return False.

The time complexity is O(n²) where n is the length of the array, as we compute the result for each possible subarray exactly once. The space complexity is also O(n²) for storing the memoized results.

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 the solution with nums = [1, 5, 233, 7].

We want to find dfs(0, 3) - the maximum score difference Player 1 can achieve from the entire array.

Step 1: Calculate dfs(0, 3) Player 1 has two choices:

  • Take left (1): gain 1, opponent plays on [5, 233, 7]
  • Take right (7): gain 7, opponent plays on [1, 5, 233]

Let's explore both paths:

Path A - Take left (1):

  • Player 1 gains: 1

  • Need to calculate dfs(1, 3) for opponent's best play on [5, 233, 7]

    Calculating dfs(1, 3):

    • Take 5: gain 5 - dfs(2, 3)
    • Take 7: gain 7 - dfs(1, 2)

    For dfs(2, 3) on [233, 7]:

    • Take 233: gain 233 - dfs(3, 3) = 233 - 7 = 226
    • Take 7: gain 7 - dfs(2, 2) = 7 - 233 = -226
    • Maximum: 226

    For dfs(1, 2) on [5, 233]:

    • Take 5: gain 5 - dfs(2, 2) = 5 - 233 = -228
    • Take 233: gain 233 - dfs(1, 1) = 233 - 5 = 228
    • Maximum: 228

    So dfs(1, 3) = max(5 - 226, 7 - 228) = max(-221, -221) = -221

    But wait, let's recalculate correctly:

    • Take 5: gain 5 - dfs(2, 3) = 5 - 226 = -221
    • Take 7: gain 7 - dfs(1, 2) = 7 - 228 = -221

    Actually, the opponent would take 7, getting dfs(1, 3) = 7 - dfs(1, 2) = 7 - 228 = -221

Path B - Take right (7):

  • Player 1 gains: 7

  • Need to calculate dfs(0, 2) for opponent's best play on [1, 5, 233]

    Calculating dfs(0, 2):

    • Take 1: gain 1 - dfs(1, 2) = 1 - 228 = -227
    • Take 233: gain 233 - dfs(0, 1) = 233 - 4 = 229

    For dfs(0, 1) on [1, 5]:

    • Take 1: gain 1 - dfs(1, 1) = 1 - 5 = -4
    • Take 5: gain 5 - dfs(0, 0) = 5 - 1 = 4
    • Maximum: 4

    So dfs(0, 2) = max(1 - 228, 233 - 4) = max(-227, 229) = 229

Final calculation for dfs(0, 3):

  • Take left: 1 - (-221) = 222
  • Take right: 7 - 229 = -222

Player 1 chooses to take left (1), achieving a maximum difference of 222.

Since 222 > 0, Player 1 wins!

Game play verification:

  1. Player 1 takes 1, leaving [5, 233, 7]
  2. Player 2 takes 7, leaving [5, 233]
  3. Player 1 takes 233, leaving [5]
  4. Player 2 takes 5

Final scores: Player 1 = 1 + 233 = 234, Player 2 = 7 + 5 = 12 Player 1 wins with difference of 222! âś“

Solution Implementation

1from typing import List
2from functools import cache
3
4class Solution:
5    def predictTheWinner(self, nums: List[int]) -> bool:
6        """
7        Determine if Player 1 can win the game.
8      
9        Args:
10            nums: List of non-negative integers representing scores
11          
12        Returns:
13            True if Player 1 can win or tie, False otherwise
14        """
15      
16        @cache
17        def dfs(left: int, right: int) -> int:
18            """
19            Calculate the maximum score difference the current player can achieve
20            over their opponent from nums[left] to nums[right].
21          
22            Args:
23                left: Left boundary index of the subarray
24                right: Right boundary index of the subarray
25              
26            Returns:
27                Maximum score difference (current player's score - opponent's score)
28            """
29            # Base case: no elements left to pick
30            if left > right:
31                return 0
32          
33            # Current player chooses between:
34            # 1. Pick nums[left] and let opponent play optimally on remaining array
35            # 2. Pick nums[right] and let opponent play optimally on remaining array
36            # The opponent's optimal play is subtracted since they try to maximize their own score
37            pick_left = nums[left] - dfs(left + 1, right)
38            pick_right = nums[right] - dfs(left, right - 1)
39          
40            # Return the maximum score difference achievable
41            return max(pick_left, pick_right)
42      
43        # Player 1 wins if their score difference is non-negative
44        return dfs(0, len(nums) - 1) >= 0
45
1class Solution {
2    // Store the input array globally for DFS access
3    private int[] nums;
4    // Memoization table to store calculated results for subproblems
5    // dp[i][j] represents the maximum score difference the current player can achieve
6    // when choosing from nums[i] to nums[j]
7    private int[][] dp;
8
9    /**
10     * Determines if Player 1 can win or tie the game
11     * @param nums Array of scores to choose from
12     * @return true if Player 1 can win or tie, false otherwise
13     */
14    public boolean predictTheWinner(int[] nums) {
15        this.nums = nums;
16        int n = nums.length;
17        dp = new int[n][n];
18      
19        // Player 1 wins or ties if their score difference is non-negative
20        return dfs(0, n - 1) >= 0;
21    }
22
23    /**
24     * Recursively calculates the maximum score difference the current player can achieve
25     * @param left Left boundary index of the current subarray
26     * @param right Right boundary index of the current subarray
27     * @return Maximum score difference (current player's score - opponent's score)
28     */
29    private int dfs(int left, int right) {
30        // Base case: no elements left to choose
31        if (left > right) {
32            return 0;
33        }
34      
35        // Return memoized result if already calculated
36        if (dp[left][right] != 0) {
37            return dp[left][right];
38        }
39      
40        // Current player chooses between:
41        // 1. Taking nums[left]: gain nums[left], opponent plays optimally on [left+1, right]
42        // 2. Taking nums[right]: gain nums[right], opponent plays optimally on [left, right-1]
43        // Since opponent also plays optimally, we subtract their best score
44        int chooseLeft = nums[left] - dfs(left + 1, right);
45        int chooseRight = nums[right] - dfs(left, right - 1);
46      
47        // Store and return the maximum score difference
48        dp[left][right] = Math.max(chooseLeft, chooseRight);
49        return dp[left][right];
50    }
51}
52
1class Solution {
2public:
3    bool predictTheWinner(vector<int>& nums) {
4        int n = nums.size();
5        // dp[i][j] represents the maximum score difference (current player - opponent)
6        // when playing optimally on subarray nums[i...j]
7        vector<vector<int>> dp(n, vector<int>(n));
8      
9        // Recursive function with memoization to calculate optimal score difference
10        auto calculateScoreDifference = [&](this auto&& calculateScoreDifference, int left, int right) -> int {
11            // Base case: no elements left to choose
12            if (left > right) {
13                return 0;
14            }
15          
16            // Return memoized result if already computed
17            if (dp[left][right] != 0) {
18                return dp[left][right];
19            }
20          
21            // Current player has two choices:
22            // 1. Take nums[left]: gain nums[left] points, opponent plays optimally on [left+1, right]
23            int takeLeft = nums[left] - calculateScoreDifference(left + 1, right);
24          
25            // 2. Take nums[right]: gain nums[right] points, opponent plays optimally on [left, right-1]
26            int takeRight = nums[right] - calculateScoreDifference(left, right - 1);
27          
28            // Store and return the maximum score difference achievable
29            return dp[left][right] = max(takeLeft, takeRight);
30        };
31      
32        // Player 1 wins if their score difference is non-negative
33        return calculateScoreDifference(0, n - 1) >= 0;
34    }
35};
36
1/**
2 * Determines if Player 1 can win the game with optimal play
3 * @param nums - Array of integers representing the game values
4 * @returns true if Player 1 can win or tie, false otherwise
5 */
6function predictTheWinner(nums: number[]): boolean {
7    const arrayLength: number = nums.length;
8  
9    // Memoization table to store the maximum score difference for subproblems
10    // memo[i][j] represents the maximum score difference the current player can achieve
11    // when choosing from nums[i] to nums[j]
12    const memo: number[][] = Array.from(
13        { length: arrayLength }, 
14        () => Array(arrayLength).fill(0)
15    );
16  
17    /**
18     * Recursively calculates the maximum score difference for the current player
19     * @param left - Left boundary index of the current subarray
20     * @param right - Right boundary index of the current subarray
21     * @returns Maximum score difference the current player can achieve
22     */
23    const calculateMaxScoreDifference = (left: number, right: number): number => {
24        // Base case: no elements left to choose
25        if (left > right) {
26            return 0;
27        }
28      
29        // Check if result is already computed (memoization)
30        if (memo[left][right] === 0) {
31            // Current player chooses the left element: nums[left]
32            // The opponent then plays optimally on the remaining array
33            const chooseLeft: number = nums[left] - calculateMaxScoreDifference(left + 1, right);
34          
35            // Current player chooses the right element: nums[right]
36            // The opponent then plays optimally on the remaining array
37            const chooseRight: number = nums[right] - calculateMaxScoreDifference(left, right - 1);
38          
39            // Store the maximum score difference in memo table
40            memo[left][right] = Math.max(chooseLeft, chooseRight);
41        }
42      
43        return memo[left][right];
44    };
45  
46    // Player 1 wins if their score difference is non-negative
47    return calculateMaxScoreDifference(0, arrayLength - 1) >= 0;
48}
49

Time and Space Complexity

Time Complexity: O(n^2)

The recursive function dfs(i, j) explores all possible subarrays of the input array nums. The parameters i and j represent the left and right boundaries of the current subarray. Since i can range from 0 to n-1 and j can range from 0 to n-1 where i ≤ j, there are approximately n^2/2 unique states. Due to the @cache decorator, each state is computed only once. For each state, the function performs constant time operations (comparison and arithmetic). Therefore, the overall time complexity is O(n^2).

Space Complexity: O(n^2)

The space complexity consists of two components:

  1. Cache storage: The @cache decorator stores the results of all unique (i, j) pairs where i ≤ j. This requires O(n^2) space to store approximately n^2/2 states.
  2. Recursion stack: In the worst case, the recursion depth can be O(n) when we recursively call from (0, n-1) down to base cases. However, this O(n) stack space is dominated by the O(n^2) cache storage.

Therefore, the total space complexity is O(n^2).

Common Pitfalls

1. Incorrectly Modeling the Score Difference

Pitfall: A common mistake is trying to track both players' scores separately or attempting to alternate between players explicitly in the recursion. This leads to complex state management and often incorrect logic.

# INCORRECT approach - trying to track whose turn it is
def dfs(left, right, is_player1_turn):
    if left > right:
        return (0, 0)  # (player1_score, player2_score)
  
    if is_player1_turn:
        # Complex logic to update player 1's score
        ...
    else:
        # Complex logic to update player 2's score
        ...

Solution: Model the problem as a score difference from the current player's perspective. Each recursive call represents the current player (whoever's turn it is), and we calculate their best achievable advantage over their opponent.

2. Forgetting to Negate the Opponent's Result

Pitfall: When the current player picks a number, the remaining subarray is played by the opponent. A common error is directly adding the opponent's result instead of subtracting it.

# INCORRECT - forgetting that opponent's gain is our loss
pick_left = nums[left] + dfs(left + 1, right)  # Wrong!
pick_right = nums[right] + dfs(left, right - 1)  # Wrong!

Solution: Remember that dfs(i, j) returns the score difference from the perspective of whoever is playing that subarray. When it's the opponent's turn, their positive difference is our negative difference:

# CORRECT - subtracting opponent's advantage
pick_left = nums[left] - dfs(left + 1, right)
pick_right = nums[right] - dfs(left, right - 1)

3. Using Incorrect Base Case Condition

Pitfall: Using left == right as the base case and returning nums[left], thinking this represents picking the last element.

# INCORRECT base case
if left == right:
    return nums[left]  # Wrong! This doesn't account for whose turn it is

Solution: Use left > right as the base case, returning 0 when no elements remain. The case where left == right should be handled by the general recursive logic, which correctly accounts for the single element being picked by the current player.

4. Not Using Memoization or Using It Incorrectly

Pitfall: Implementing the recursive solution without memoization, leading to exponential time complexity, or manually implementing memoization incorrectly.

# INCORRECT - no memoization, exponential time
def dfs(left, right):
    if left > right:
        return 0
    return max(nums[left] - dfs(left + 1, right),
               nums[right] - dfs(left, right - 1))

# INCORRECT - wrong memoization key
memo = {}
def dfs(left, right):
    if nums[left] in memo:  # Wrong key!
        return memo[nums[left]]
    ...

Solution: Use @cache decorator or properly implement memoization with (left, right) as the key:

# Using functools.cache (recommended)
@cache
def dfs(left, right):
    ...

# Or manual memoization
memo = {}
def dfs(left, right):
    if (left, right) in memo:
        return memo[(left, right)]
    # ... calculate result ...
    memo[(left, right)] = result
    return result
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