877. Stone Game
Problem Description
Alice and Bob are playing a strategic game with stones. The game setup consists of an even number of piles arranged in a row, where each pile contains a positive integer number of stones represented by piles[i]
.
The game rules are:
- Players take turns, with Alice going first
- On each turn, a player must take an entire pile of stones from either the beginning or the end of the row
- Players continue taking piles until no piles remain
- The winner is the player who collects the most stones in total
- Since the total number of stones is odd, there cannot be a tie
Both players play optimally, meaning they always make the best possible move to maximize their chances of winning.
The task is to determine whether Alice wins the game. Return true
if Alice wins, or false
if Bob wins.
For example, if piles = [5, 3, 4, 5]
:
- Alice could take 5 from the beginning, leaving [3, 4, 5]
- Bob then takes from either end
- The game continues with both players making optimal choices
- The function should return whether Alice ends up with more stones than Bob
The solution uses dynamic programming with memoization to calculate the maximum advantage Alice can achieve. The dfs(i, j)
function computes the maximum score difference (current player's score minus opponent's score) when choosing optimally from piles between indices i
and j
. At each state, the player chooses between taking the left pile (piles[i]
) or the right pile (piles[j]
), considering that the opponent will also play optimally in subsequent turns.
Intuition
When facing this problem, we need to think about how to model optimal play for both players. The key insight is that each player wants to maximize their own score while minimizing their opponent's score.
Let's think about what happens when a player faces a subarray of piles from index i
to j
. The player has two choices:
- Take the left pile (
piles[i]
) and leave piles fromi+1
toj
for the opponent - Take the right pile (
piles[j]
) and leave piles fromi
toj-1
for the opponent
Here's the crucial observation: instead of tracking both players' scores separately, we can track the score difference between the current player and their opponent. This simplifies our state representation significantly.
When we take piles[i]
, we gain piles[i]
points, but then our opponent plays optimally on the remaining piles [i+1, j]
. If we denote the optimal score difference for the remaining subproblem as dfs(i+1, j)
, then our opponent will achieve that difference. From our perspective, this means we get piles[i] - dfs(i+1, j)
.
Why the subtraction? Because dfs(i+1, j)
represents how much better the player (now our opponent) does compared to their opponent (us) in that subgame. So we subtract it from our gain.
Similarly, if we take piles[j]
, our score difference becomes piles[j] - dfs(i, j-1)
.
Since we play optimally, we choose the option that gives us the maximum score difference:
dfs(i, j) = max(piles[i] - dfs(i+1, j), piles[j] - dfs(i, j-1))
The base case occurs when i > j
, meaning no piles are left, so the score difference is 0
.
Finally, Alice wins if dfs(0, len(piles)-1) > 0
, meaning she can achieve a positive score difference when playing optimally from the beginning.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization using Python's @cache
decorator.
Implementation Details:
-
Main Function Structure:
- The
stoneGame
function takes the list of piles as input - It defines an inner recursive function
dfs(i, j)
that calculates the maximum score difference for the current player
- The
-
Recursive Function
dfs(i, j)
:- Parameters:
i
andj
represent the left and right boundaries of the remaining piles - Base Case: When
i > j
, no piles remain, so return0
- Recursive Case: Calculate the maximum of two choices:
- Take left pile:
piles[i] - dfs(i + 1, j)
- Take right pile:
piles[j] - dfs(i, j - 1)
- Take left pile:
- Parameters:
-
Memoization:
- The
@cache
decorator automatically memoizes the results ofdfs(i, j)
- This prevents redundant calculations for the same subproblems
- Without memoization, the time complexity would be exponential; with it, we achieve
O(n²)
time complexity
- The
-
State Space:
- There are
O(n²)
possible states wheren
is the number of piles - Each state
(i, j)
represents a unique subarray of piles
- There are
-
Final Decision:
- Call
dfs(0, len(piles) - 1)
to get Alice's maximum advantage when starting with all piles - Return
True
if this value is positive (Alice wins), otherwiseFalse
- Call
Time and Space Complexity:
- Time Complexity:
O(n²)
wheren
is the number of piles, as we compute each state(i, j)
at most once - Space Complexity:
O(n²)
for the memoization cache plusO(n)
for the recursion stack
The beauty of this approach is that it naturally handles the alternating turns by recursively computing score differences, eliminating the need to track whose turn it is or maintain separate scores for each player.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with piles = [5, 3, 4, 5]
to illustrate how the dynamic programming solution works.
We'll use dfs(i, j)
to represent the maximum score difference (current player's score - opponent's score) when choosing optimally from piles between indices i
and j
.
Initial call: dfs(0, 3)
- Alice plays first with all 4 piles available.
At dfs(0, 3)
, Alice has two choices:
- Take left pile (5): Score difference =
5 - dfs(1, 3)
- Take right pile (5): Score difference =
5 - dfs(0, 2)
Let's evaluate dfs(1, 3)
(piles [3, 4, 5]):
- Take left (3):
3 - dfs(2, 3)
- Take right (5):
5 - dfs(1, 2)
For dfs(2, 3)
(piles [4, 5]):
- Take left (4):
4 - dfs(3, 3)
=4 - 5
=-1
- Take right (5):
5 - dfs(2, 2)
=5 - 4
=1
- Maximum:
1
For dfs(1, 2)
(piles [3, 4]):
- Take left (3):
3 - dfs(2, 2)
=3 - 4
=-1
- Take right (4):
4 - dfs(1, 1)
=4 - 3
=1
- Maximum:
1
So dfs(1, 3) = max(3 - 1, 5 - 1) = max(2, 4) = 4
Similarly, we can calculate dfs(0, 2)
(piles [5, 3, 4]):
- Take left (5):
5 - dfs(1, 2)
=5 - 1
=4
- Take right (4):
4 - dfs(0, 1)
=4 - 2
=2
- Maximum:
4
Finally, dfs(0, 3) = max(5 - 4, 5 - 4) = max(1, 1) = 1
Since dfs(0, 3) = 1 > 0
, Alice wins the game. The positive value means Alice can secure 1 more stone than Bob when both play optimally.
In this example, one optimal play sequence would be:
- Alice takes 5 (left), leaving [3, 4, 5]
- Bob takes 5 (right), leaving [3, 4]
- Alice takes 4 (right), leaving [3]
- Bob takes 3 Final scores: Alice = 9, Bob = 8
Solution Implementation
1class Solution:
2 def stoneGame(self, piles: List[int]) -> bool:
3 """
4 Determine if Alice can win the stone game.
5
6 Alice and Bob take turns picking stones from either end of the array.
7 Alice goes first. Both play optimally.
8
9 Args:
10 piles: List of integers representing stone piles
11
12 Returns:
13 True if Alice wins (gets more stones than Bob), False otherwise
14 """
15 from functools import cache
16
17 @cache
18 def calculate_score_difference(left: int, right: int) -> int:
19 """
20 Calculate the maximum score difference the current player can achieve.
21
22 The score difference represents how many more stones the current player
23 can get compared to their opponent when both play optimally.
24
25 Args:
26 left: Left boundary index of remaining piles
27 right: Right boundary index of remaining piles
28
29 Returns:
30 Maximum score difference achievable by current player
31 """
32 # Base case: no piles left to pick
33 if left > right:
34 return 0
35
36 # Current player chooses optimally between:
37 # 1. Taking left pile: gain piles[left], opponent gets the difference from remaining
38 # 2. Taking right pile: gain piles[right], opponent gets the difference from remaining
39 take_left = piles[left] - calculate_score_difference(left + 1, right)
40 take_right = piles[right] - calculate_score_difference(left, right - 1)
41
42 # Return the maximum score difference achievable
43 return max(take_left, take_right)
44
45 # Alice wins if her score difference is positive
46 return calculate_score_difference(0, len(piles) - 1) > 0
47
1class Solution {
2 // Array to store the stone piles
3 private int[] piles;
4 // Memoization table for dynamic programming
5 // dp[i][j] represents the maximum score difference the current player can achieve
6 // when choosing from piles[i] to piles[j]
7 private int[][] dp;
8
9 /**
10 * Determines if the first player can win the stone game.
11 *
12 * @param piles Array of stone piles where each element represents the number of stones
13 * @return true if the first player can win, false otherwise
14 */
15 public boolean stoneGame(int[] piles) {
16 this.piles = piles;
17 int n = piles.length;
18 dp = new int[n][n];
19
20 // First player wins if their score difference is positive
21 return dfs(0, n - 1) > 0;
22 }
23
24 /**
25 * Recursive function with memoization to calculate the maximum score difference
26 * the current player can achieve.
27 *
28 * @param left Left boundary index of remaining piles
29 * @param right Right boundary index of remaining piles
30 * @return Maximum score difference the current player can achieve
31 */
32 private int dfs(int left, int right) {
33 // Base case: no piles left to choose from
34 if (left > right) {
35 return 0;
36 }
37
38 // Return memoized result if already calculated
39 if (dp[left][right] != 0) {
40 return dp[left][right];
41 }
42
43 // Current player chooses the pile that maximizes their score difference
44 // If choosing left pile: gain piles[left] and opponent plays optimally on remaining piles
45 // If choosing right pile: gain piles[right] and opponent plays optimally on remaining piles
46 int chooseLeft = piles[left] - dfs(left + 1, right);
47 int chooseRight = piles[right] - dfs(left, right - 1);
48
49 // Store and return the maximum score difference
50 dp[left][right] = Math.max(chooseLeft, chooseRight);
51 return dp[left][right];
52 }
53}
54
1class Solution {
2public:
3 bool stoneGame(vector<int>& piles) {
4 int n = piles.size();
5
6 // dp[i][j] represents the maximum score difference (current player - opponent)
7 // when playing optimally on piles from index i to j
8 int dp[n][n];
9 memset(dp, 0, sizeof(dp));
10
11 // Recursive function with memoization to calculate the maximum score difference
12 // Parameters: i - left boundary index, j - right boundary index
13 // Returns: maximum score difference the current player can achieve
14 auto calculateMaxDifference = [&](this auto&& calculateMaxDifference, int i, int j) -> int {
15 // Base case: no piles left to choose
16 if (i > j) {
17 return 0;
18 }
19
20 // Return memoized result if already calculated
21 if (dp[i][j] != 0) {
22 return dp[i][j];
23 }
24
25 // Current player has two choices:
26 // 1. Take the left pile (piles[i]) and subtract opponent's best score from remaining piles
27 // 2. Take the right pile (piles[j]) and subtract opponent's best score from remaining piles
28 int takeLeft = piles[i] - calculateMaxDifference(i + 1, j);
29 int takeRight = piles[j] - calculateMaxDifference(i, j - 1);
30
31 // Store and return the maximum score difference
32 return dp[i][j] = max(takeLeft, takeRight);
33 };
34
35 // Alice wins if her score difference is positive
36 return calculateMaxDifference(0, n - 1) > 0;
37 }
38};
39
1/**
2 * Determines if the first player can win the stone game
3 * @param piles - Array of stone piles where each element represents the number of stones in that pile
4 * @returns true if the first player can win, false otherwise
5 */
6function stoneGame(piles: number[]): boolean {
7 const pileCount: number = piles.length;
8
9 // Memoization table to store computed results
10 // memo[i][j] represents the maximum score difference the current player can achieve
11 // when choosing from piles[i] to piles[j]
12 const memo: number[][] = new Array(pileCount)
13 .fill(0)
14 .map(() => new Array(pileCount).fill(0));
15
16 /**
17 * Recursively calculates the maximum score difference for the current player
18 * @param left - Left boundary index of available piles
19 * @param right - Right boundary index of available piles
20 * @returns Maximum score difference the current player can achieve
21 */
22 const calculateMaxScoreDifference = (left: number, right: number): number => {
23 // Base case: no piles left to choose from
24 if (left > right) {
25 return 0;
26 }
27
28 // Check if result is already computed (memoization)
29 if (memo[left][right] === 0) {
30 // Choose left pile: gain piles[left] stones,
31 // then subtract opponent's best score from remaining piles
32 const chooseLeft: number = piles[left] - calculateMaxScoreDifference(left + 1, right);
33
34 // Choose right pile: gain piles[right] stones,
35 // then subtract opponent's best score from remaining piles
36 const chooseRight: number = piles[right] - calculateMaxScoreDifference(left, right - 1);
37
38 // Store the maximum score difference in memo table
39 memo[left][right] = Math.max(chooseLeft, chooseRight);
40 }
41
42 return memo[left][right];
43 };
44
45 // First player wins if their score difference is positive
46 return calculateMaxScoreDifference(0, pileCount - 1) > 0;
47}
48
Time and Space Complexity
Time Complexity: O(n²)
The solution uses dynamic programming with memoization. The dfs
function explores all possible game states defined by the range [i, j]
. There are n
possible values for i
(from 0 to n-1) and n
possible values for j
(from 0 to n-1). However, only states where i ≤ j
are valid, which gives us approximately n²/2
unique states. Each state is computed only once due to the @cache
decorator, and each computation takes O(1)
time (just comparing two recursive calls). Therefore, the overall time complexity is O(n²)
.
Space Complexity: O(n²)
The space complexity consists of two components:
- Memoization cache: The
@cache
decorator stores the results of all unique(i, j)
pairs wherei ≤ j
. This requiresO(n²)
space to store approximatelyn²/2
states. - Recursion call stack: In the worst case, the recursion depth can go up to
n
levels (when we process from one end of the array to the other). This addsO(n)
to the space complexity.
Since O(n²) + O(n) = O(n²)
, the overall space complexity is O(n²)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Score Calculation Logic
Pitfall: A common mistake is trying to track both players' scores separately or incorrectly handling the alternating turns. Developers often write something like:
# INCORRECT approach
def dfs(i, j, is_alice_turn):
if i > j:
return 0
if is_alice_turn:
# Trying to maximize Alice's score
return max(piles[i] + dfs(i+1, j, False),
piles[j] + dfs(i, j-1, False))
else:
# Trying to minimize Alice's score (Bob's turn)
return min(dfs(i+1, j, True),
dfs(i, j-1, True))
Why it's wrong: This approach fails because it doesn't properly account for both players' scores. You'd need to return and track both scores, making the logic more complex.
Solution: Use the score difference approach where each recursive call returns current_player_score - opponent_score
. This elegantly handles alternating turns:
def dfs(i, j):
if i > j:
return 0
# The subtraction naturally handles the alternation
return max(piles[i] - dfs(i+1, j),
piles[j] - dfs(i, j-1))
2. Missing or Incorrect Memoization
Pitfall: Forgetting to add memoization or implementing it incorrectly:
# INCORRECT - No memoization
def stoneGame(self, piles):
def dfs(i, j):
if i > j:
return 0
return max(piles[i] - dfs(i+1, j),
piles[j] - dfs(i, j-1))
return dfs(0, len(piles)-1) > 0
Why it's wrong: Without memoization, the time complexity becomes exponential O(2^n) because the same subproblems are calculated multiple times.
Solution: Always use memoization for overlapping subproblems:
from functools import cache
@cache # or use lru_cache(None)
def dfs(i, j):
# recursive logic here
3. Mathematical Insight Oversight
Pitfall: Not recognizing that Alice can always win when there's an even number of piles.
Mathematical Proof: With an even number of piles, Alice can always force a win by choosing either all odd-indexed or all even-indexed piles. She can determine which set has more stones at the start and force that outcome.
Optimal Solution: For this specific problem, you could simply return True
:
def stoneGame(self, piles):
return True
However, the dynamic programming solution is more general and works even if the rules were modified (e.g., odd number of piles or different winning conditions).
4. Index Boundary Errors
Pitfall: Incorrect base case or index handling:
# INCORRECT - Wrong base case
def dfs(i, j):
if i == j: # Wrong! Should be i > j
return piles[i] # This would double-count the last pile
Solution: The correct base case is if i > j: return 0
because when left index exceeds right index, there are no piles left to pick.
5. Forgetting to Clear Cache Between Test Cases
Pitfall: When testing multiple instances, the cache from previous test cases might interfere:
# Potential issue in testing environment
cache = {}
def dfs(i, j):
if (i, j) in cache:
return cache[(i, j)]
# ... recursive logic
Solution: Either use function-local memoization with @cache
decorator (which creates a new cache for each function instance) or explicitly clear the cache between test cases if using a global cache.
Which type of traversal does breadth first search do?
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!