1690. Stone Game VII
Problem Description
Alice and Bob are playing a stone game where they take turns, with Alice going first.
The game setup:
- There are
n
stones arranged in a row, each with a value given bystones[i]
- On each turn, a player can remove either the leftmost or rightmost stone from the row
- When a player removes a stone, they receive points equal to the sum of all remaining stones' values
- The game continues until all stones are removed
The scoring works like this: If a player removes a stone when there are other stones left, their score increases by the sum of those remaining stones. For example, if stones are [5, 3, 1, 4]
and a player removes the leftmost stone (5), they get 3 + 1 + 4 = 8
points.
Both players play optimally, but with different objectives:
- Alice wants to maximize the score difference (her score minus Bob's score)
- Bob wants to minimize the score difference (since he knows he'll lose, he wants to lose by as little as possible)
Given an array stones
where stones[i]
represents the value of the i-th
stone from the left, you need to return the difference between Alice's and Bob's scores when both play optimally.
For example, if stones = [5, 3, 1, 4, 2]
:
- The game might proceed with players strategically choosing which stones to remove
- Each choice affects not only their own score but also limits the opponent's future options
- The final result would be the difference: (Alice's total score) - (Bob's total score)
Intuition
When thinking about this game, we need to consider that both players are playing optimally but with opposing goals. This is a classic game theory problem where we need to think recursively about the best moves.
The key insight is that at any state of the game, we can represent it by which stones remain. If stones from index i
to j
are still in the row, the current player needs to decide whether to take the leftmost stone at index i
or the rightmost stone at index j
.
When a player removes a stone, they score points equal to the sum of the remaining stones. This means if we remove stone at index i
, we get the sum of stones from i+1
to j
. If we remove stone at index j
, we get the sum of stones from i
to j-1
. We can efficiently calculate these sums using a prefix sum array.
The crucial realization is that this is a zero-sum game - one player's gain is the other player's loss. After a player makes their move and scores some points, the game state changes and it becomes the opponent's turn. The opponent will then try to maximize their own advantage from that new state.
This leads us to think about the problem recursively: for any game state (i, j)
, we want to find the maximum score difference the current player can achieve. When the current player takes a stone and scores some points, they add those points to their score, but then the opponent plays optimally from the resulting state. So the score difference is:
- Points gained from this move
- Minus the best score difference the opponent can achieve from the resulting state
Since the opponent is also trying to maximize their own score difference from their perspective, we need to subtract their optimal result from our gain. This gives us the recurrence relation where dfs(i, j)
represents the maximum score difference the current player can achieve when stones from index i
to j
remain.
The base case is when i > j
, meaning no stones are left, so the score difference is 0
.
To avoid recalculating the same game states multiple times, we use memoization to cache the results of each (i, j)
state we compute.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with memoization to solve this game theory problem. Here's how the implementation works:
Step 1: Prefix Sum Calculation
First, we create a prefix sum array s
using accumulate(stones, initial=0)
. This gives us:
s[0] = 0
s[i] = stones[0] + stones[1] + ... + stones[i-1]
This allows us to calculate the sum of any subarray in O(1) time. For stones from index i
to j
, the sum is s[j+1] - s[i]
.
Step 2: Recursive Function with Memoization
We define dfs(i, j)
to represent the maximum score difference the current player can achieve when stones from index i
to j
remain.
The function works as follows:
-
Base Case: If
i > j
, there are no stones left, so return0
. -
Recursive Case: The current player has two choices:
-
Remove the leftmost stone (at index
i
):- The player scores
s[j+1] - s[i+1]
(sum of stones fromi+1
toj
) - The game continues with stones from
i+1
toj
, where the opponent plays - The score difference becomes:
s[j+1] - s[i+1] - dfs(i+1, j)
- We subtract
dfs(i+1, j)
because the opponent will maximize their own score difference from that state
- The player scores
-
Remove the rightmost stone (at index
j
):- The player scores
s[j] - s[i]
(sum of stones fromi
toj-1
) - The game continues with stones from
i
toj-1
- The score difference becomes:
s[j] - s[i] - dfs(i, j-1)
- The player scores
-
-
The player chooses the option that maximizes their score difference:
max(a, b)
Step 3: Memoization with @cache
The @cache
decorator automatically memoizes the function results, preventing redundant calculations for the same (i, j)
pairs. This reduces the time complexity from exponential to O(n²).
Step 4: Final Answer
We call dfs(0, len(stones) - 1)
to get the maximum score difference when starting with all stones. After getting the result, we clear the cache with dfs.cache_clear()
to free memory.
The time complexity is O(n²) because we have at most n² unique states (i, j)
, and each state is computed once. The space complexity is also O(n²) for storing the memoization cache.
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 a small example with stones = [3, 7, 2]
to illustrate how the solution works.
Initial Setup:
- Prefix sum array:
s = [0, 3, 10, 12]
s[0] = 0
s[1] = 3
s[2] = 3 + 7 = 10
s[3] = 3 + 7 + 2 = 12
Game Tree (Alice starts first):
We need to compute dfs(0, 2)
- the maximum score difference when all stones are available.
Computing dfs(0, 2): Alice has two choices:
-
Remove left stone (value 3):
- Alice scores:
s[3] - s[1] = 12 - 3 = 9
(sum of remaining stones [7, 2]) - Game continues with
dfs(1, 2)
(Bob's turn) - Score difference:
9 - dfs(1, 2)
- Alice scores:
-
Remove right stone (value 2):
- Alice scores:
s[2] - s[0] = 10 - 0 = 10
(sum of remaining stones [3, 7]) - Game continues with
dfs(0, 1)
(Bob's turn) - Score difference:
10 - dfs(0, 1)
- Alice scores:
Computing dfs(1, 2): Bob has two choices with stones [7, 2]:
-
Remove left stone (value 7):
- Bob scores:
s[3] - s[2] = 12 - 10 = 2
(sum of remaining stone [2]) - Game continues with
dfs(2, 2)
(Alice's turn) - Score difference:
2 - dfs(2, 2)
- Bob scores:
-
Remove right stone (value 2):
- Bob scores:
s[2] - s[1] = 10 - 3 = 7
(sum of remaining stone [7]) - Game continues with
dfs(1, 1)
(Alice's turn) - Score difference:
7 - dfs(1, 1)
- Bob scores:
Computing dfs(2, 2): Only stone at index 2 remains:
- Remove it: score =
s[3] - s[3] = 0
(no other stones left) - Result:
0 - dfs(3, 2) = 0 - 0 = 0
Computing dfs(1, 1): Only stone at index 1 remains:
- Remove it: score =
s[2] - s[2] = 0
(no other stones left) - Result:
0 - dfs(2, 1) = 0 - 0 = 0
Back to dfs(1, 2):
- Option 1:
2 - 0 = 2
- Option 2:
7 - 0 = 7
- Bob chooses max:
dfs(1, 2) = 7
Computing dfs(0, 1): Bob has two choices with stones [3, 7]:
- Remove left stone (value 3): scores
7
, thendfs(1, 1) = 0
, difference =7 - 0 = 7
- Remove right stone (value 7): scores
3
, thendfs(0, 0) = 0
, difference =3 - 0 = 3
- Bob chooses max:
dfs(0, 1) = 7
Final computation of dfs(0, 2):
- Option 1:
9 - 7 = 2
- Option 2:
10 - 7 = 3
- Alice chooses max:
dfs(0, 2) = 3
Result: The maximum score difference Alice can achieve is 3.
Verification: If Alice removes the right stone first (optimal play):
- Alice scores 10 (removes stone 2, gets sum of [3,7])
- Bob scores 7 (removes stone 7, gets sum of [3])
- Alice scores 0 (removes stone 3, no stones left)
- Total: Alice = 10, Bob = 7, Difference = 3 ✓
Solution Implementation
1class Solution:
2 def stoneGameVII(self, stones: List[int]) -> int:
3 """
4 Alice and Bob play a game with stones. They take turns removing stones from either end.
5 The score for each turn is the sum of remaining stones after removal.
6 Alice wants to maximize the score difference, Bob wants to minimize it.
7 Returns the maximum score difference Alice can achieve with optimal play.
8
9 Args:
10 stones: List of integers representing stone values
11
12 Returns:
13 Maximum score difference (Alice's score - Bob's score)
14 """
15 from functools import cache
16 from itertools import accumulate
17 from typing import List
18
19 @cache
20 def dfs(left: int, right: int) -> int:
21 """
22 Calculate the maximum score difference the current player can achieve
23 from the subarray stones[left:right+1].
24
25 Args:
26 left: Left boundary index of the current stone range
27 right: Right boundary index of the current stone range
28
29 Returns:
30 Maximum score difference the current player can achieve
31 """
32 # Base case: no stones left to pick
33 if left > right:
34 return 0
35
36 # Option 1: Remove leftmost stone
37 # Score gained: sum of remaining stones (excluding the removed one)
38 # Subtract opponent's best score from the remaining subarray
39 remove_left_score = prefix_sum[right + 1] - prefix_sum[left + 1] - dfs(left + 1, right)
40
41 # Option 2: Remove rightmost stone
42 # Score gained: sum of remaining stones (excluding the removed one)
43 # Subtract opponent's best score from the remaining subarray
44 remove_right_score = prefix_sum[right] - prefix_sum[left] - dfs(left, right - 1)
45
46 # Choose the option that maximizes the current player's advantage
47 return max(remove_left_score, remove_right_score)
48
49 # Build prefix sum array for O(1) range sum queries
50 # prefix_sum[i] = sum of stones[0:i]
51 prefix_sum = list(accumulate(stones, initial=0))
52
53 # Calculate the maximum score difference Alice can achieve
54 result = dfs(0, len(stones) - 1)
55
56 # Clear the cache to free memory
57 dfs.cache_clear()
58
59 return result
60
1class Solution {
2 // Prefix sum array to calculate range sums efficiently
3 private int[] prefixSum;
4 // Memoization table for dynamic programming
5 private Integer[][] memo;
6
7 public int stoneGameVII(int[] stones) {
8 int n = stones.length;
9
10 // Initialize prefix sum array (1-indexed for easier calculation)
11 prefixSum = new int[n + 1];
12 // Initialize memoization table
13 memo = new Integer[n][n];
14
15 // Build prefix sum array: prefixSum[i+1] = sum of stones[0...i]
16 for (int i = 0; i < n; ++i) {
17 prefixSum[i + 1] = prefixSum[i] + stones[i];
18 }
19
20 // Start the game with all stones available (indices 0 to n-1)
21 return dfs(0, n - 1);
22 }
23
24 /**
25 * Calculate the maximum score difference the current player can achieve
26 * @param left The leftmost index of remaining stones
27 * @param right The rightmost index of remaining stones
28 * @return The maximum score difference (current player's score - opponent's score)
29 */
30 private int dfs(int left, int right) {
31 // Base case: no stones remaining
32 if (left > right) {
33 return 0;
34 }
35
36 // Return memoized result if already calculated
37 if (memo[left][right] != null) {
38 return memo[left][right];
39 }
40
41 // Option 1: Remove leftmost stone
42 // Current player gets sum of remaining stones (excluding the removed one)
43 // Then subtract opponent's best score from the resulting state
44 int removeLeft = (prefixSum[right + 1] - prefixSum[left + 1]) - dfs(left + 1, right);
45
46 // Option 2: Remove rightmost stone
47 // Current player gets sum of remaining stones (excluding the removed one)
48 // Then subtract opponent's best score from the resulting state
49 int removeRight = (prefixSum[right] - prefixSum[left]) - dfs(left, right - 1);
50
51 // Choose the option that maximizes the score difference
52 // Store the result in memoization table before returning
53 return memo[left][right] = Math.max(removeLeft, removeRight);
54 }
55}
56
1class Solution {
2public:
3 int stoneGameVII(vector<int>& stones) {
4 int n = stones.size();
5
6 // dp[i][j] represents the maximum score difference the current player can achieve
7 // when playing optimally on stones[i..j]
8 int dp[n][n];
9 memset(dp, 0, sizeof(dp));
10
11 // prefixSum[i] stores the sum of stones[0..i-1]
12 // This allows us to calculate range sums in O(1) time
13 int prefixSum[n + 1];
14 prefixSum[0] = 0;
15 for (int i = 0; i < n; ++i) {
16 prefixSum[i + 1] = prefixSum[i] + stones[i];
17 }
18
19 // Recursive function with memoization to calculate the maximum score difference
20 function<int(int, int)> dfs = [&](int left, int right) {
21 // Base case: no stones left to pick
22 if (left > right) {
23 return 0;
24 }
25
26 // Return memoized result if already calculated
27 if (dp[left][right]) {
28 return dp[left][right];
29 }
30
31 // Option 1: Remove leftmost stone (stones[left])
32 // Current player gets sum of remaining stones: stones[left+1..right]
33 // Then subtract opponent's best score from the remaining subproblem
34 int removeLeft = (prefixSum[right + 1] - prefixSum[left + 1]) - dfs(left + 1, right);
35
36 // Option 2: Remove rightmost stone (stones[right])
37 // Current player gets sum of remaining stones: stones[left..right-1]
38 // Then subtract opponent's best score from the remaining subproblem
39 int removeRight = (prefixSum[right] - prefixSum[left]) - dfs(left, right - 1);
40
41 // Choose the option that maximizes the score difference
42 return dp[left][right] = max(removeLeft, removeRight);
43 };
44
45 // Start the game with all stones available
46 return dfs(0, n - 1);
47 }
48};
49
1/**
2 * Calculates the maximum score difference between Alice and Bob in Stone Game VII
3 * @param stones - Array of stone values
4 * @returns The maximum score difference Alice can achieve
5 */
6function stoneGameVII(stones: number[]): number {
7 const stonesCount: number = stones.length;
8
9 // Build prefix sum array for quick range sum calculation
10 // prefixSum[i] represents sum of stones[0] to stones[i-1]
11 const prefixSum: number[] = Array(stonesCount + 1).fill(0);
12 for (let i = 0; i < stonesCount; ++i) {
13 prefixSum[i + 1] = prefixSum[i] + stones[i];
14 }
15
16 // Memoization table for dynamic programming
17 // memo[i][j] stores the maximum score difference for subarray stones[i..j]
18 const memo: number[][] = Array.from({ length: stonesCount }, () => Array(stonesCount).fill(0));
19
20 /**
21 * Recursively calculates the maximum score difference for a given range
22 * @param left - Left boundary index (inclusive)
23 * @param right - Right boundary index (inclusive)
24 * @returns Maximum score difference the current player can achieve
25 */
26 const calculateMaxDifference = (left: number, right: number): number => {
27 // Base case: no stones left to pick
28 if (left > right) {
29 return 0;
30 }
31
32 // Return memoized result if already calculated
33 if (memo[left][right] !== 0) {
34 return memo[left][right];
35 }
36
37 // Option 1: Remove leftmost stone
38 // Current player gets sum of remaining stones minus opponent's best score
39 const removeLeftScore: number = prefixSum[right + 1] - prefixSum[left + 1] - calculateMaxDifference(left + 1, right);
40
41 // Option 2: Remove rightmost stone
42 // Current player gets sum of remaining stones minus opponent's best score
43 const removeRightScore: number = prefixSum[right] - prefixSum[left] - calculateMaxDifference(left, right - 1);
44
45 // Store and return the maximum of both options
46 memo[left][right] = Math.max(removeLeftScore, removeRightScore);
47 return memo[left][right];
48 };
49
50 // Start the game with all stones
51 return calculateMaxDifference(0, stonesCount - 1);
52}
53
Time and Space Complexity
The time complexity of this code is O(n^2)
, where n
is the number of stones.
The recursive function dfs(i, j)
explores all possible subarray ranges [i, j]
of the stones array. Since i
can range from 0
to n-1
and j
can range from 0
to n-1
with the constraint i <= j
, there are approximately n^2/2
unique states. Due to the @cache
decorator, each state is computed only once. The work done per state is O(1)
(calculating prefix sums and making recursive calls that are already cached). Therefore, the overall time complexity is O(n^2)
.
The space complexity is O(n^2)
. This comes from two sources:
- The memoization cache stores the result for each unique
(i, j)
pair, which requiresO(n^2)
space - The recursion stack depth in the worst case is
O(n)
when we recursively remove stones one by one - The prefix sum array
s
takesO(n)
space
Since O(n^2)
dominates, the total space complexity is O(n^2)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Score Calculation - Using Stone Value Instead of Remaining Sum
A very common mistake is misunderstanding how scoring works in this problem. Many people incorrectly assume that when you remove a stone, you get points equal to that stone's value.
Wrong Understanding:
# INCORRECT: Thinking you score the value of the removed stone score_when_removing_left = stones[i] - dfs(i+1, j) score_when_removing_right = stones[j] - dfs(i, j-1)
Correct Understanding:
# CORRECT: You score the sum of ALL REMAINING stones after removal
score_when_removing_left = sum(stones[i+1:j+1]) - dfs(i+1, j)
score_when_removing_right = sum(stones[i:j]) - dfs(i, j-1)
The problem states: "When a player removes a stone, they receive points equal to the sum of all remaining stones' values." This means if you remove a stone, you DON'T get that stone's value - you get the sum of everything else that's left.
2. Inefficient Sum Calculation Without Prefix Sums
Computing the sum of remaining stones repeatedly leads to O(n³) time complexity:
Inefficient Approach:
def dfs(i, j):
if i > j:
return 0
# O(n) operation for each recursive call
remove_left_score = sum(stones[i+1:j+1]) - dfs(i+1, j)
remove_right_score = sum(stones[i:j]) - dfs(i, j-1)
return max(remove_left_score, remove_right_score)
Efficient Solution with Prefix Sums:
# Precompute prefix sums once
prefix_sum = list(accumulate(stones, initial=0))
def dfs(i, j):
if i > j:
return 0
# O(1) operation using prefix sums
remove_left_score = prefix_sum[j+1] - prefix_sum[i+1] - dfs(i+1, j)
remove_right_score = prefix_sum[j] - prefix_sum[i] - dfs(i, j-1)
return max(remove_left_score, remove_right_score)
3. Confusing Score Difference with Individual Scores
Another pitfall is trying to track Alice's and Bob's individual scores separately, rather than working with the score difference directly.
Overcomplicated Approach:
def dfs(i, j, is_alice_turn):
if i > j:
return (0, 0) # (alice_score, bob_score)
if is_alice_turn:
# Calculate Alice's best move...
alice_score, bob_score = ...
else:
# Calculate Bob's best move...
alice_score, bob_score = ...
return (alice_score, bob_score)
Cleaner Difference-Based Approach:
def dfs(i, j):
# Returns the maximum score difference the current player can achieve
if i > j:
return 0
# Each player maximizes their own score difference
return max(
score_from_left - dfs(i+1, j),
score_from_right - dfs(i, j-1)
)
The key insight is that both players use the same strategy: maximize their own score minus opponent's score. This allows us to use a single recursive function without tracking whose turn it is.
4. Forgetting to Clear Cache (Memory Leak in Multiple Test Cases)
When using @cache
decorator in platforms that run multiple test cases, forgetting to clear the cache can cause memory issues:
# Without cache clearing - potential memory leak
def stoneGameVII(self, stones):
@cache
def dfs(i, j):
# ... implementation ...
return dfs(0, len(stones) - 1)
# Cache persists across test cases!
# With proper cache clearing
def stoneGameVII(self, stones):
@cache
def dfs(i, j):
# ... implementation ...
result = dfs(0, len(stones) - 1)
dfs.cache_clear() # Important for multiple test cases
return result
Which of these properties could exist for a graph but not a tree?
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!