Stone Game VIII
HardPrompt
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, while the number of stones is more than one, they will do the following:
- Choose an integer
x > 1, and remove the leftmostxstones from the row. - Add the sum of the removed stones' values to the player's score.
- Place a new stone, whose value is equal to that sum, on the left side of the row.
The game stops when only one stone is left in the row.
The score difference between Alice and Bob is (Alice's score - Bob's score). Alice's goal is to maximize the score difference, and Bob's goal is the minimize the score difference.
Given an integer array stones of length n where stones[i] represents the value of the ith stone from the left, return the score difference between Alice and Bob if they both play optimally.
Example 1:
Input: stones = [-1,2,-3,4,-5]
Output: 5
Explanation:
- Alice removes the first 4 stones, adds (-1) + 2 + (-3) + 4 = 2 to her score, and places a stone of
value 2 on the left. stones = [2,-5].
- Bob removes the first 2 stones, adds 2 + (-5) = -3 to his score, and places a stone of value -3 on
the left. stones = [-3].
The difference between their scores is 2 - (-3) = 5.Example 2:
Input: stones = [7,-6,5,10,5,-2,-6]
Output: 13
Explanation:
- Alice removes all stones, adds 7 + (-6) + 5 + 10 + 5 + (-2) + (-6) = 13 to her score, and places a
stone of value 13 on the left. stones = [13].
The difference between their scores is 13 - 0 = 13.Example 3:
Input: stones = [-10,-12]
Output: -22
Explanation:
- Alice can only make one move, which is to remove both stones. She adds (-10) + (-12) = -22 to her
score and places a stone of value -22 on the left. stones = [-22].
The difference between their scores is (-22) - 0 = -22.
Constraints:
n == stones.length2 <= n <= 105-104 <= stones[i] <= 104
Approaches
2 approaches with complexity analysis and trade-offs.
This problem can be modeled as a minimax game. Since both players play optimally, we can use dynamic programming to find the best possible outcome for Alice. The key is to simplify the game state. A crucial observation is that any move results in a new stone whose value is a prefix sum of the original stones array. This allows us to define the state of the game by a single index i, representing that the current row of stones is [prefix[i], s_i, s_{i+1}, ..., s_{n-1}], where prefix[i] is the sum of the first i original stones.
Let dp[i] be the maximum score difference the current player can achieve starting from state i. The goal is to compute the result for Alice's first move.
Algorithm
- First, precompute the prefix sums of the
stonesarray. Letprefix[k]be the sum ofstones[0]throughstones[k-1]. - Let
dp[i]be the maximum score difference the current player can achieve starting from a game state where the stones are effectively[prefix[i], stones[i], ..., stones[n-1]]. - The base case is
dp[n] = 0, as statenimplies the game has ended for the player whose turn it is. - We compute the
dptable backwards fromi = n-1down to1. - The recurrence relation is
dp[i] = max_{j=i+1}^{n} (prefix[j] - dp[j]). This is because from statei, a player can choose to transition to any statej > i, gainingprefix[j]points and giving the turn to the opponent, who will then scoredp[j]from the new state. - The final answer is
dp[1], which represents Alice's optimal score difference from her first move choices.
Walkthrough
First, we precompute the prefix sums of the stones array. Let prefix[k] be the sum of the first k stones.
prefix[k] = stones[0] + ... + stones[k-1].
The DP state dp[i] represents the maximum score difference the current player can obtain when the game is in a state where the stones are effectively [prefix[i], stones[i], ..., stones[n-1]]. The player whose turn it is can choose to merge the first k stones, where k > 1. This corresponds to choosing an index j from i+1 to n.
If the player chooses j, they merge the first stone prefix[i] with the next j-i stones (stones[i] to stones[j-1]). The sum of these stones is prefix[j]. This becomes the player's score for this turn. The game then transitions to state j, and it's the other player's turn. The other player will play optimally from state j and achieve a score difference of dp[j]. Therefore, the current player's score difference for choosing j is prefix[j] - dp[j].
Since the player wants to maximize their score difference, they will choose j that maximizes this value. This gives us the recurrence relation:
dp[i] = max_{j=i+1}^{n} (prefix[j] - dp[j])
The base case is dp[n] = 0, as state n means there's only one stone left (prefix[n]), and the game has ended for the player who is about to move.
We can compute the dp table backwards from i = n-1 down to 1.
The final answer is the maximum score difference Alice can achieve on her first move. She starts with the original stones array. She can choose to merge the first x stones, where x is from 2 to n. This gives her a score of prefix[x] and transitions the game to state x for Bob. Bob will then achieve dp[x]. Alice's net difference is prefix[x] - dp[x]. She will choose x to maximize this.
The answer is max_{x=2}^{n} (prefix[x] - dp[x]), which is exactly dp[1].
class Solution { public int stoneGameVIII(int[] stones) { int n = stones.length; long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + stones[i]; } long[] dp = new long[n + 1]; // dp[n] is implicitly 0 for (int i = n - 1; i >= 1; i--) { long maxVal = Long.MIN_VALUE; for (int j = i + 1; j <= n; j++) { maxVal = Math.max(maxVal, prefix[j] - dp[j]); } dp[i] = maxVal; } return (int) dp[1]; }}Complexity
Time
O(n^2) - Calculating prefix sums takes O(n). The nested loops for the DP calculation take O(n^2) time.
Space
O(n) - We use O(n) space for the prefix sum array and O(n) for the dp array.
Trade-offs
Pros
It's a direct and intuitive translation of the game's recurrence relation.
It correctly solves the problem for smaller constraints.
Cons
The
O(n^2)time complexity is too slow for the given constraints (n <= 10^5), resulting in a 'Time Limit Exceeded' error on most platforms.
The O(n^2) DP approach can be optimized by observing a relationship between dp[i] and dp[i-1]. This optimization reduces the time complexity to linear, making it efficient enough to pass the given constraints.
Algorithm
- First, create a prefix sum array
prefixof sizen+1. - The recurrence
dp[i] = max_{j=i+1}^{n} (prefix[j] - dp[j])can be optimized. By observing thatdp[i-1] = max(prefix[i] - dp[i], dp[i]), we can compute each DP state in O(1) time. - We can compute the DP values backwards. The base case is
dp[n-1] = prefix[n](since from staten-1, the only move is to take all remaining stones, scoringprefix[n]). - We can use a single variable, say
dp_val, to store the DP value of the next state (dp[i+1]) while computing the current state (dp[i]). - Initialize
dp_val = prefix[n]. This representsdp[n-1]. - Iterate
ifromn-2down to1.- In each step, update
dp_valusing the optimized recurrence:dp_val = max(dp_val, prefix[i+1] - dp_val). This newdp_valisdp[i].
- In each step, update
- After the loop,
dp_valwill hold the value ofdp[1], which is the final answer.
Walkthrough
The recurrence relation from the previous approach is:
dp[i] = max_{j=i+1}^{n} (prefix[j] - dp[j])
Let's write out the expressions for dp[i] and dp[i-1]:
dp[i] = max(prefix[i+1] - dp[i+1], prefix[i+2] - dp[i+2], ..., prefix[n] - dp[n])
dp[i-1] = max(prefix[i] - dp[i], prefix[i+1] - dp[i+1], ..., prefix[n] - dp[n])
Notice that the terms in the max for dp[i-1] are prefix[i] - dp[i] and all the terms from the max for dp[i]. This means we can rewrite dp[i-1] in terms of dp[i]:
dp[i-1] = max(prefix[i] - dp[i], dp[i])
This simplified recurrence allows us to compute each dp value in O(1) time, given the next value. We can still compute the dp table backwards.
The base case remains dp[n] = 0. For i = n-1, the only choice is j=n, so dp[n-1] = prefix[n] - dp[n] = prefix[n]. Then, we can iterate from i = n-2 down to 1 using the optimized recurrence: dp[i] = max(prefix[i+1] - dp[i+1], dp[i+1]). The final answer is still dp[1].
This approach can be further space-optimized. Since dp[i] only depends on dp[i+1], we don't need to store the entire dp array. We can use a single variable to keep track of the previous dp value (dp[i+1]) while computing the current one (dp[i]).
class Solution { public int stoneGameVIII(int[] stones) { int n = stones.length; long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + stones[i]; } // dp[i] represents the optimal score difference for the current player // starting from state i. // State i: stones are [prefix[i], stones[i], ..., stones[n-1]] // The final answer is dp[1]. // We can use a single variable to store the dp value as we iterate backwards. // Let's call it `dp_val`. It will hold dp[i+1] at the start of the loop for i. // Base case: dp[n-1] = prefix[n]. // (The only move from state n-1 is to take all remaining stones, score prefix[n]) long dp_val = prefix[n]; // Iterate from i = n-2 down to 1 to compute dp[n-2], ..., dp[1]. for (int i = n - 2; i >= 1; i--) { // At this point, dp_val holds dp[i+1]. // We compute dp[i] = max(dp[i+1], prefix[i+1] - dp[i+1]) // and update dp_val to be dp[i] for the next iteration. dp_val = Math.max(dp_val, prefix[i + 1] - dp_val); } return (int) dp_val; }}Complexity
Time
O(n) - We iterate once to compute prefix sums and once more for the DP calculation.
Space
O(n) - The prefix sum array requires O(n) space. The DP calculation itself uses O(1) extra space.
Trade-offs
Pros
Highly efficient with linear time complexity.
Solves the problem within the given constraints.
The space-optimized version is memory efficient.
Cons
The derivation of the optimized recurrence is less intuitive than the straightforward O(n^2) DP approach.
Solutions
Solution
class Solution {private Integer[] f;private int[] s;private int n;public int stoneGameVIII(int[] stones) { n = stones.length; f = new Integer[n]; for (int i = 1; i < n; ++i) { stones[i] += stones[i - 1]; } s = stones; return dfs(1); }private int dfs(int i) { if (i >= n - 1) { return s[i]; } if (f[i] == null) { f[i] = Math.max(dfs(i + 1), s[i] - dfs(i + 1)); } return f[i]; }}class Solution {public: int stoneGameVIII(vector<int> &stones) { int n = stones.size(); for (int i = 1; i < n; ++i) { stones[i] += stones[i - 1]; } int f[n]; memset(f, -1, sizeof(f)); function<int(int)> dfs = [&](int i) -> int { if (i >= n - 1) { return stones[i]; } if (f[i] == -1) { f[i] = max(dfs(i + 1), stones[i] - dfs(i + 1)); } return f[i]; }; return dfs(1); }};class Solution: def stoneGameVIII(self, stones: List[int]) -> int: @ cache def dfs(i: int) -> int: if i >= len(stones) - 1: return s[- 1] return max(dfs(i + 1), s[i] - dfs(i + 1)) s = list(accumulate(stones)) return dfs(1)Video 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.