Stone Game
MedPrompt
Alice and Bob play a game with piles of stones. There are an even number of piles arranged in a row, and each pile has a positive integer number of stones piles[i].
The objective of the game is to end with the most stones. The total number of stones across all the piles is odd, so there are no ties.
Alice and Bob take turns, with Alice starting first. Each turn, a player takes the entire pile of stones either from the beginning or from the end of the row. This continues until there are no more piles left, at which point the person with the most stones wins.
Assuming Alice and Bob play optimally, return true if Alice wins the game, or false if Bob wins.
Example 1:
Input: piles = [5,3,4,5]
Output: true
Explanation:
Alice starts first, and can only take the first 5 or the last 5.
Say she takes the first 5, so that the row becomes [3, 4, 5].
If Bob takes 3, then the board is [4, 5], and Alice takes 5 to win with 10 points.
If Bob takes the last 5, then the board is [3, 4], and Alice takes 4 to win with 9 points.
This demonstrated that taking the first 5 was a winning move for Alice, so we return true.Example 2:
Input: piles = [3,7,2,3]
Output: true
Constraints:
2 <= piles.length <= 500piles.lengthis even.1 <= piles[i] <= 500sum(piles[i])is odd.
Approaches
4 approaches with complexity analysis and trade-offs.
This approach models the game using a recursive function that represents the choices of the current player. At each step, a player chooses the pile that maximizes their final score relative to the opponent. This is a classic minimax strategy. The function calculates the maximum score difference the current player can achieve from a given sub-array of piles.
Algorithm
- Create a recursive helper function
solve(piles, i, j)that returns the maximum score difference a player can get from pilesitoj. - Base Case: If
i == j, there's only one pile left. The current player takes it, so returnpiles[i]. - Recursive Step: The current player has two choices:
- Take
piles[i]: Their score difference will bepiles[i]minus the score difference the next player gets from the remaining piles[i+1, j]. This ispiles[i] - solve(piles, i + 1, j). - Take
piles[j]: Similarly, the score difference ispiles[j] - solve(piles, i, j - 1).
- Take
- The player chooses the move that maximizes their score difference. So,
solve(i, j)returnsmax(choice1, choice2). - The initial call is
solve(piles, 0, piles.length - 1). Alice wins if this result is greater than 0.
Walkthrough
We define a recursive function, say solve(i, j), which returns the maximum score difference the current player can obtain from the piles in the range [i, j]. The current player can either take piles[i] or piles[j]. If the player takes piles[i], their score increases by piles[i]. The opponent will then play on the subproblem [i+1, j] and will get a score difference of solve(i+1, j). So, the current player's net score difference from this move is piles[i] - solve(i+1, j). Similarly, if the player takes piles[j], their net score difference is piles[j] - solve(i, j-1). Since the player plays optimally, they will choose the move that maximizes their score difference. The initial call is solve(0, n-1). Alice wins if this value is greater than 0. This approach recomputes the same subproblems multiple times, leading to an exponential time complexity.
class Solution { public boolean stoneGame(int[] piles) { return solve(piles, 0, piles.length - 1) > 0; } private int solve(int[] piles, int i, int j) { if (i == j) { return piles[i]; } int takeFirst = piles[i] - solve(piles, i + 1, j); int takeLast = piles[j] - solve(piles, i, j - 1); return Math.max(takeFirst, takeLast); }}Complexity
Time
O(2^n), where n is the number of piles. For each subproblem, we make two recursive calls, leading to an exponential number of operations.
Space
O(n), where n is the number of piles. This is due to the maximum depth of the recursion call stack.
Trade-offs
Pros
Conceptually simple and directly translates the game's rules into code.
Serves as a good foundation for more optimized dynamic programming solutions.
Cons
Extremely inefficient due to a large number of redundant calculations for the same subproblems.
Will result in a 'Time Limit Exceeded' error on most platforms for non-trivial input sizes.
This approach optimizes the brute-force recursion by using dynamic programming to avoid recomputing results for the same subproblems. We can implement this using either a top-down (memoization) or bottom-up (tabulation) method. Both will have the same time and space complexity. This approach correctly solves the general version of the stone game.
Algorithm
- The logic is based on solving subproblems of increasing length.
- Create a 2D DP table
dpof sizen x n, wheredp[i][j]will store the max score difference forpiles[i...j]. - Iterate
len(subproblem length) from 1 ton. - Inside, iterate
i(start index) from 0 ton - len. - Calculate
j = i + len - 1(end index). - Base Case (
len=1):dp[i][i] = piles[i]. - Recursive Step (
len>1):dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]). - After the loops,
dp[0][n-1]holds the result for the entire game. Returndp[0][n-1] > 0.
Walkthrough
The core idea is to solve for smaller sub-ranges of piles first and use those results to solve for larger ranges. Let dp[i][j] be the maximum score difference the current player can achieve from the piles in the range [i, j]. The recurrence relation is dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]).
Top-Down (Memoization): This is a direct optimization of the recursive solution. We use a 2D array memo to store the results of solve(i, j). Before computing, we check the memo table. If the result exists, we return it; otherwise, we compute it, store it, and then return.
// Top-Down DP with Memoizationclass Solution { public boolean stoneGame(int[] piles) { int n = piles.length; Integer[][] memo = new Integer[n][n]; return solve(piles, 0, n - 1, memo) > 0; } private int solve(int[] piles, int i, int j, Integer[][] memo) { if (i > j) return 0; if (memo[i][j] != null) return memo[i][j]; int takeFirst = piles[i] - solve(piles, i + 1, j, memo); int takeLast = piles[j] - solve(piles, i, j - 1, memo); memo[i][j] = Math.max(takeFirst, takeLast); return memo[i][j]; }}Bottom-Up (Tabulation): This approach builds the solution iteratively. We fill a 2D dp table, usually starting from the smallest subproblems (length 1) and building up to the full problem (length n).
// Bottom-Up DPclass Solution { public boolean stoneGame(int[] piles) { int n = piles.length; int[][] dp = new int[n][n]; for (int i = 0; i < n; i++) { dp[i][i] = piles[i]; } for (int len = 2; len <= n; len++) { for (int i = 0; i <= n - len; i++) { int j = i + len - 1; dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]); } } return dp[0][n - 1] > 0; }}Complexity
Time
O(n^2), as there are O(n^2) subproblems `(i, j)`, and each is computed once in constant time.
Space
O(n^2) for the DP table (`dp` or `memo`). The recursive stack space in the memoized version is also O(n), making the total O(n^2).
Trade-offs
Pros
Guarantees an optimal solution by exploring all possibilities efficiently.
Much more efficient than brute-force recursion, making it feasible for the given constraints.
Avoids recursion overhead and potential stack overflow in the bottom-up version.
Cons
Requires O(n^2) space, which can be substantial for large
n.While optimal for the general problem, it's overkill for this specific version with its special constraints.
We can optimize the space complexity of the bottom-up DP approach from O(n^2) to O(n). When calculating the values for a certain subproblem length, we only need the results from the immediately smaller length. This dependency allows us to discard older results and use only a 1D DP array.
Algorithm
- Create a 1D DP array
dpof sizen. - Initialize
dpwith the values ofpiles(this represents the base case for subproblems of length 1). - Iterate
lenfrom 2 ton. - Inside, iterate
ifrom 0 ton - len. - Update
dp[i]using the recurrencedp[i] = max(piles[i] - dp[i+1], piles[j] - dp[i]), wherej = i + len - 1. - The values
dp[i+1]anddp[i]on the right side refer to the values from the previousleniteration. - After the loops,
dp[0]will hold the final result for the whole game. Returndp[0] > 0.
Walkthrough
Instead of a 2D dp table, we use a 1D array, say dp[n]. dp[i] will store the result for the subproblem of the current length starting at index i. We iterate over len from 2 to n. In the inner loop, we update the dp array for the current length. The recurrence becomes dp[i] = max(piles[i] - dp[i+1], piles[j] - dp[i]). Note that when we calculate the new dp[i], the dp[i+1] and dp[i] on the right side are values from the previous length's calculation. We initialize the dp array with the base cases for len=1: dp[i] = piles[i]. Then, we loop len from 2 to n and i from 0 to n-len, updating dp[i] based on the formula.
class Solution { public boolean stoneGame(int[] piles) { int n = piles.length; int[] dp = new int[n]; for (int i = 0; i < n; i++) { dp[i] = piles[i]; } for (int len = 2; len <= n; len++) { for (int i = 0; i <= n - len; i++) { int j = i + len - 1; dp[i] = Math.max(piles[i] - dp[i + 1], piles[j] - dp[i]); } } return dp[0] > 0; }}Complexity
Time
O(n^2). The nested loops for `len` and `i` remain the same as in the 2D DP approach.
Space
O(n) for the 1D DP array.
Trade-offs
Pros
More space-efficient than the standard DP approach.
Maintains the O(n^2) time efficiency while reducing memory footprint.
Cons
The logic for updating the 1D array can be slightly harder to reason about compared to the 2D DP table.
Time complexity is still O(n^2).
This approach leverages the specific constraints of the problem to arrive at a solution without simulating the game. By analyzing the game's structure (even number of piles, odd total sum), we can prove that the first player, Alice, always has a winning strategy.
Algorithm
- Analyze the game's constraints:
piles.lengthis even, andsum(piles)is odd. - Realize that Alice, the first player, can always force a win.
- She can choose to collect either all the stones from the piles at even indices or all the stones from piles at odd indices.
- Since the total sum is odd, the sum of even-indexed piles and odd-indexed piles cannot be equal.
- Alice can calculate both sums and choose the strategy that yields the larger sum.
- Therefore, Alice always wins. Return
true.
Walkthrough
The key insight lies in partitioning the piles based on their original indices: even-indexed piles (piles[0], piles[2], ...) and odd-indexed piles (piles[1], piles[3], ...). Let sum_even be the total stones in even-indexed piles and sum_odd be the total in odd-indexed piles. The total number of stones, sum_even + sum_odd, is given to be odd. This implies that sum_even cannot be equal to sum_odd.
Alice, as the first player, can enforce a strategy. Because the number of piles n is even, the first pile (piles[0]) and the last pile (piles[n-1]) have different index parities (even and odd). Alice can choose one. After her move, Bob is left with two ends that have the same original index parity. No matter which one Bob chooses, he will leave Alice with one even-indexed end and one odd-indexed end on her next turn. This pattern continues.
This means Alice can decide at the start: 'I will only take piles from even original indices' or 'I will only take piles from odd original indices'. She can always execute her chosen strategy.
- Strategy 1: Take
piles[0]first, and on every subsequent turn, take the available pile that was at an even index. Her total score will besum_even. - Strategy 2: Take
piles[n-1]first, and on every subsequent turn, take the available pile that was at an odd index. Her total score will besum_odd.
Since sum_even != sum_odd, Alice can simply choose the strategy that corresponds to the larger sum. As she can guarantee she gets the larger share, and there are no ties, she always wins.
class Solution { public boolean stoneGame(int[] piles) { return true; }}Complexity
Time
O(1). The solution is based on a mathematical proof and is independent of the input size.
Space
O(1). No extra space is used.
Trade-offs
Pros
Extremely efficient, providing an instant solution.
Demonstrates a deep understanding of the game's mathematical properties.
Requires no extra space.
Cons
This solution is highly specific to the given constraints and would not work if the constraints were different (e.g., an odd number of piles or an even total sum).
Solutions
Solution
class Solution {public boolean stoneGame(int[] piles) { int n = piles.length; int[][] f = new int[n][n]; for (int i = 0; i < n; ++i) { f[i][i] = piles[i]; } for (int i = n - 2; i >= 0; --i) { for (int j = i + 1; j < n; ++j) { f[i][j] = Math.max(piles[i] - f[i + 1][j], piles[j] - f[i][j - 1]); } } return f[0][n - 1] > 0; }}class Solution {public: bool stoneGame(vector<int> &piles) { int n = piles.size(); int f[n][n]; memset(f, 0, sizeof(f)); for (int i = 0; i < n; ++i) { f[i][i] = piles[i]; } for (int i = n - 2; ~i; --i) { for (int j = i + 1; j < n; ++j) { f[i][j] = max(piles[i] - f[i + 1][j], piles[j] - f[i][j - 1]); } } return f[0][n - 1] > 0; }};class Solution: def stoneGame(self, piles: List[int]) -> bool: n = len(piles) f = [[0] * n for _ in range(n)] for i, x in enumerate(piles): f[i][i] = x for i in range(n - 2, - 1, - 1): for j in range(i + 1, n): f[i][j] = max(piles[i] - f[i + 1][j], piles[j] - f[i][j - 1]) return f[0][n - 1] > 0Video 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.