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]
ornums[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
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:
-
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 ofdfs(i+1, j)
- The net difference for the current player is:
nums[i] - dfs(i+1, j)
- The player gains
-
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 ofdfs(i, j-1)
- The net difference for the current player is:
nums[j] - dfs(i, j-1)
- The player gains
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 EvaluatorExample 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:
- Player 1 takes 1, leaving [5, 233, 7]
- Player 2 takes 7, leaving [5, 233]
- Player 1 takes 233, leaving [5]
- 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:
- Cache storage: The
@cache
decorator stores the results of all unique(i, j)
pairs wherei ≤ j
. This requiresO(n^2)
space to store approximatelyn^2/2
states. - 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, thisO(n)
stack space is dominated by theO(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
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
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
Want a Structured Path to Master System Design Too? Don’t Miss This!