Stone Game VII
MedPrompt
Alice and Bob take turns playing a game, with Alice starting first.
There are n stones arranged in a row. On each player's turn, they can remove either the leftmost stone or the rightmost stone from the row and receive points equal to the sum of the remaining stones' values in the row. The winner is the one with the higher score when there are no stones left to remove.
Bob found that he will always lose this game (poor Bob, he always loses), so he decided to minimize the score's difference. Alice's goal is to maximize the difference in the score.
Given an array of integers stones where stones[i] represents the value of the ith stone from the left, return the difference in Alice and Bob's score if they both play optimally.
Example 1:
Input: stones = [5,3,1,4,2]
Output: 6
Explanation:
- Alice removes 2 and gets 5 + 3 + 1 + 4 = 13 points. Alice = 13, Bob = 0, stones = [5,3,1,4].
- Bob removes 5 and gets 3 + 1 + 4 = 8 points. Alice = 13, Bob = 8, stones = [3,1,4].
- Alice removes 3 and gets 1 + 4 = 5 points. Alice = 18, Bob = 8, stones = [1,4].
- Bob removes 1 and gets 4 points. Alice = 18, Bob = 12, stones = [4].
- Alice removes 4 and gets 0 points. Alice = 18, Bob = 12, stones = [].
The score difference is 18 - 12 = 6.Example 2:
Input: stones = [7,90,5,1,100,10,10,2]
Output: 122
Constraints:
n == stones.length2 <= n <= 10001 <= stones[i] <= 1000
Approaches
4 approaches with complexity analysis and trade-offs.
This approach optimizes the space complexity of the bottom-up DP solution. By observing the state transitions, we can see that to compute the values for a subarray of a certain length, we only need the results for subarrays of the immediately smaller length. This means to compute the current row i of our DP table, we only need the next row i+1. This dependency allows us to reduce the space from O(n^2) to O(n).
Algorithm
- Create a prefix sum array for
O(1)sum calculations. - Create a 1D DP array
dp[n], initialized to 0. - Iterate the start index
ifromn-2down to0. - In the inner loop, iterate the end index
jfromi+1up ton-1. - Inside the inner loop, calculate the two possible differences:
a.
diff1 = (prefixSum[j+1] - prefixSum[i+1]) - dp[j]. Heredp[j]holds the value from the previous outer loop, which corresponds todp[i+1][j]. b.diff2 = (prefixSum[j] - prefixSum[i]) - dp[j-1]. Heredp[j-1]holds the value just computed in the current inner loop, which corresponds todp[i][j-1]. - Update
dp[j]withmax(diff1, diff2). - After the loops,
dp[n-1]will contain the result for the entire arraystones[0...n-1].
Walkthrough
The recurrence is dp[i][j] = max(sum(i+1, j) - dp[i+1][j], sum(i, j-1) - dp[i][j-1]). Let's iterate by the starting index i from n-2 down to 0, and the ending index j from i+1 up to n-1. When we compute dp[i][j], we need dp[i+1][j] (from the 'next' row i+1) and dp[i][j-1] (from the 'current' row i, but a previous column). This structure allows for space optimization. We can use a 1D array, say dp[n]. Let dp[j] represent the value for the current row i and column j. When we are calculating the row for i, the dp array will hold the values from the previous iteration, which corresponds to row i+1. The recurrence becomes: dp[j] = max(sum(i+1, j) - dp[j], sum(i, j-1) - dp[j-1]), where we update the dp array in place.
class Solution { public int stoneGameVII(int[] stones) { int n = stones.length; int[] prefixSum = new int[n + 1]; for (int i = 0; i < n; i++) { prefixSum[i + 1] = prefixSum[i] + stones[i]; } int[] dp = new int[n]; // i is the starting index of the subarray for (int i = n - 2; i >= 0; i--) { // j is the ending index of the subarray for (int j = i + 1; j < n; j++) { // Sum of stones from i+1 to j int sumTakeLeft = prefixSum[j + 1] - prefixSum[i + 1]; // Sum of stones from i to j-1 int sumTakeRight = prefixSum[j] - prefixSum[i]; // dp[j] currently holds dp[i+1][j] from the previous outer loop iteration // dp[j-1] holds dp[i][j-1] from the current inner loop iteration int diff1 = sumTakeLeft - dp[j]; int diff2 = sumTakeRight - dp[j - 1]; dp[j] = Math.max(diff1, diff2); } } return dp[n - 1]; }}Complexity
Time
O(n^2). The nested loops run O(n^2) times, and each step is O(1).
Space
O(n). We use a 1D array `dp` of size `n` and a prefix sum array of size `n+1`.
Trade-offs
Pros
Most efficient in terms of space complexity.
Maintains the optimal O(n^2) time complexity.
Cons
The logic for in-place updates and loop directions can be tricky to reason about compared to the O(n^2) space solution.
This approach directly translates the game's rules into a recursive function. The function calculates the score difference for a given sub-array of stones. At each step, it explores two possibilities: taking the leftmost stone or the rightmost stone. It then recursively calls itself for the remaining sub-array and the next player. Since it's a minimax problem, the current player chooses the move that maximizes their score difference relative to the opponent.
Algorithm
- Create a prefix sum array to quickly calculate the sum of any subarray.
- Define a recursive function
solve(i, j)that computes the maximum score difference for the subarraystones[i...j]. - The base case for the recursion is when
i >= j, meaning there is one or zero stone left. In this case, the game ends for this subproblem, and the difference is 0. - In the recursive step, calculate the outcome of two possible moves:
a. Removing
stones[i]: The score gained issum(i+1, j). The opponent will then play onstones[i+1...j]and achieve a difference ofsolve(i+1, j). The current player's total difference issum(i+1, j) - solve(i+1, j). b. Removingstones[j]: The score gained issum(i, j-1). The opponent will play onstones[i...j-1]and achievesolve(i, j-1). The current player's total difference issum(i, j-1) - solve(i, j-1). - The function
solve(i, j)returns the maximum of the two outcomes, as the current player plays optimally to maximize the difference. - The final answer is the result of
solve(0, n-1).
Walkthrough
We define a recursive function, say solve(i, j), which returns the maximum score difference the current player can get from the subarray stones[i...j]. The total score of the stones from index i to j is needed for calculating the points. We can pre-calculate prefix sums for efficiency. Let sum(i, j) = prefix[j+1] - prefix[i]. In the solve(i, j) function:
- Base Case: If
i > j(no stones left), the score difference is 0. - Recursive Step: The current player has two choices:
- Take
stones[i]: The player getssum(i+1, j)points. The game continues on the subarraystones[i+1...j], and it's the opponent's turn. The opponent will play optimally to get a difference ofsolve(i+1, j). So, the current player's net difference for this move issum(i+1, j) - solve(i+1, j). - Take
stones[j]: The player getssum(i, j-1)points. The game continues onstones[i...j-1]. The opponent will get a difference ofsolve(i, j-1). The current player's net difference issum(i, j-1) - solve(i, j-1).
- Take
The current player wants to maximize their difference, so they choose the maximum of these two outcomes. This approach recomputes the same subproblems multiple times, leading to an exponential time complexity.
class Solution { private int[] prefixSum; public int stoneGameVII(int[] stones) { int n = stones.length; this.prefixSum = new int[n + 1]; for (int i = 0; i < n; i++) { prefixSum[i + 1] = prefixSum[i] + stones[i]; } return solve(0, n - 1); } private int getSum(int i, int j) { if (i > j) { return 0; } return prefixSum[j + 1] - prefixSum[i]; } private int solve(int i, int j) { if (i >= j) { return 0; } // Option 1: Take the leftmost stone stones[i] int scoreTakeLeft = getSum(i + 1, j); int diff1 = scoreTakeLeft - solve(i + 1, j); // Option 2: Take the rightmost stone stones[j] int scoreTakeRight = getSum(i, j - 1); int diff2 = scoreTakeRight - solve(i, j - 1); return Math.max(diff1, diff2); }}Complexity
Time
O(2^n). For each subproblem of size `k`, we make two recursive calls for subproblems of size `k-1`. This creates a binary recursion tree of depth `n`.
Space
O(n) for the recursion stack depth and O(n) for the prefix sum array. Total O(n).
Trade-offs
Pros
Simple to understand and implement as it directly follows the game's logic.
Cons
Extremely inefficient due to a large number of overlapping subproblems.
Will result in a "Time Limit Exceeded" (TLE) error for larger inputs.
This approach improves upon the brute-force recursion by storing the results of subproblems in a memoization table (a 2D array). Before computing the result for a subproblem (i, j), we first check if it has already been solved. If so, we return the stored value. Otherwise, we compute it, store it, and then return it. This avoids redundant computations.
Algorithm
- Initialize a prefix sum array for
O(1)sum calculations. - Initialize a 2D memoization array
memo[n][n]with a sentinel value to store results of subproblems. - Define a recursive function
solve(i, j). - Inside
solve(i, j), first check ifmemo[i][j]contains a valid result. If so, return it. - The base case is
i >= j, return 0. - Otherwise, compute the two possible outcomes by taking the leftmost or rightmost stone, just like in the recursive approach.
a.
diff1 = sum(i+1, j) - solve(i+1, j)b.diff2 = sum(i, j-1) - solve(i, j-1) - Store the maximum of
diff1anddiff2inmemo[i][j]. - Return the stored value.
- The initial call is
solve(0, n-1).
Walkthrough
The recursive structure is the same as the brute-force approach. We introduce a 2D array, memo[n][n], to store the results of solve(i, j). Initialize it with a value indicating that the state has not been computed (e.g., null or a sentinel value like -1). In the solve(i, j) function, we first check if memo[i][j] has been computed. If yes, we return memo[i][j]. If not, we perform the recursive calculations as before and store the result in memo[i][j] before returning it. This technique is also known as memoization and is a top-down dynamic programming approach.
class Solution { private int[] prefixSum; private Integer[][] memo; public int stoneGameVII(int[] stones) { int n = stones.length; this.prefixSum = new int[n + 1]; for (int i = 0; i < n; i++) { prefixSum[i + 1] = prefixSum[i] + stones[i]; } this.memo = new Integer[n][n]; return solve(0, n - 1); } private int getSum(int i, int j) { if (i > j) { return 0; } return prefixSum[j + 1] - prefixSum[i]; } private int solve(int i, int j) { if (i >= j) { return 0; } if (memo[i][j] != null) { return memo[i][j]; } int scoreTakeLeft = getSum(i + 1, j); int diff1 = scoreTakeLeft - solve(i + 1, j); int scoreTakeRight = getSum(i, j - 1); int diff2 = scoreTakeRight - solve(i, j - 1); memo[i][j] = Math.max(diff1, diff2); return memo[i][j]; }}Complexity
Time
O(n^2). There are `n * (n+1) / 2` possible states `(i, j)`. Each state is computed only once. The computation for each state takes O(1) time (excluding the recursive calls, which are memoized).
Space
O(n^2) for the memoization table `memo`. The recursion stack depth can go up to O(n). So, the total space is dominated by the memoization table.
Trade-offs
Pros
Much more efficient than brute-force recursion.
Guaranteed to pass within the time limits for the given constraints.
Often more intuitive to write than the bottom-up approach.
Cons
Uses O(n^2) space, which might be a concern for very large n.
Can lead to stack overflow for very deep recursion, although not an issue with n <= 1000.
This is an iterative version of the dynamic programming solution. We use a 2D array dp[n][n] where dp[i][j] stores the maximum score difference the current player can achieve from the subarray stones[i...j]. We fill this table starting from smaller subproblems (smaller length subarrays) and build up to the solution for the entire array.
Algorithm
- Create a prefix sum array for
O(1)sum calculations. - Create a 2D DP table
dp[n][n], initialized to 0.dp[i][j]will store the max difference forstones[i...j]. - Iterate over the subarray length
lenfrom 2 ton. - For each
len, iterate through all possible start indicesifrom 0 ton - len. - Calculate the end index
j = i + len - 1. - Calculate the sum of the remaining stones for both possible moves:
a. If taking
stones[i], remaining sum issum(i+1, j). b. If takingstones[j], remaining sum issum(i, j-1). - Use the DP recurrence to fill
dp[i][j]:dp[i][j] = max(sum(i+1, j) - dp[i+1][j], sum(i, j-1) - dp[i][j-1]). - After the loops complete,
dp[0][n-1]will hold the final answer.
Walkthrough
The state transition is the same: dp[i][j] = max(sum(i+1, j) - dp[i+1][j], sum(i, j-1) - dp[i][j-1]). We can see that dp[i][j] depends on solutions for smaller subarrays: dp[i+1][j] and dp[i][j-1]. This suggests we can iterate over the length of the subarray, len, from 2 to n. For each len, we iterate through all possible starting indices i. The ending index j will be i + len - 1. The base cases are subarrays of length 0 or 1, for which the difference is 0. So, dp[i][i] is 0. The final answer will be in dp[0][n-1].
class Solution { public int stoneGameVII(int[] stones) { int n = stones.length; int[] prefixSum = new int[n + 1]; for (int i = 0; i < n; i++) { prefixSum[i + 1] = prefixSum[i] + stones[i]; } int[][] dp = new int[n][n]; // len is the length of the subarray for (int len = 2; len <= n; len++) { // i is the starting index of the subarray for (int i = 0; i <= n - len; i++) { int j = i + len - 1; // Sum of stones from i+1 to j int sumTakeLeft = prefixSum[j + 1] - prefixSum[i + 1]; // Sum of stones from i to j-1 int sumTakeRight = prefixSum[j] - prefixSum[i]; // Calculate the difference for both choices int diff1 = sumTakeLeft - dp[i + 1][j]; int diff2 = sumTakeRight - dp[i][j - 1]; dp[i][j] = Math.max(diff1, diff2); } } return dp[0][n - 1]; }}Complexity
Time
O(n^2). We have two nested loops to fill the O(n^2) states in the DP table. Each state calculation is O(1).
Space
O(n^2) for the DP table `dp` and `O(n)` for the prefix sum array.
Trade-offs
Pros
Avoids recursion, so no risk of stack overflow.
Efficient and guaranteed to pass.
Can be slightly faster than memoization in practice due to no function call overhead.
Cons
Still uses O(n^2) space.
The iteration logic can sometimes be less intuitive to derive than the recursive solution.
Solutions
Solution
class Solution {private int[] s;private Integer[][] f;public int stoneGameVII(int[] stones) { int n = stones.length; s = new int[n + 1]; f = new Integer[n][n]; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + stones[i]; } return dfs(0, n - 1); }private int dfs(int i, int j) { if (i > j) { return 0; } if (f[i][j] != null) { return f[i][j]; } int a = s[j + 1] - s[i + 1] - dfs(i + 1, j); int b = s[j] - s[i] - dfs(i, j - 1); return f[i][j] = Math.max(a, b); }}class Solution {public: int stoneGameVII(vector<int> &stones) { int n = stones.size(); int f[n][n]; memset(f, 0, sizeof f); int s[n + 1]; s[0] = 0; for (int i = 0; i < n; ++i) { s[i + 1] = s[i] + stones[i]; } function<int(int, int)> dfs = [&](int i, int j) { if (i > j) { return 0; } if (f[i][j]) { return f[i][j]; } int a = s[j + 1] - s[i + 1] - dfs(i + 1, j); int b = s[j] - s[i] - dfs(i, j - 1); return f[i][j] = max(a, b); }; return dfs(0, n - 1); }};class Solution: def stoneGameVII(self, stones: List[int]) -> int: @ cache def dfs(i, j): if i > j: return 0 a = s[j + 1] - s[i + 1] - dfs(i + 1, j) b = s[j] - s[i] - dfs(i, j - 1) return max(a, b) s = list(accumulate(stones, initial=0)) ans = dfs(0, len(stones) - 1) dfs . cache_clear() return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.