1140. Stone Game II - Explanation
Description
Alice and Bob continue their games with piles of stones. There are a 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.
Alice and Bob take turns, with Alice starting first.
On each player's turn, that player can take all the stones in the first X remaining piles, where 1 <= X <= 2M. Then, we set M = max(M, X). Initially, M = 1.
The game continues until all the stones have been taken.
Assuming Alice and Bob play optimally, return the maximum number of stones Alice can get.
Example 1:
Input: piles = [3,1,2,5,7]
Output: 10Explanation: Alice takes first pile, then Bob takes the second pile, then Alice takes the next two piles and Bob takes the last remaining pile. This makes Alice's score 3 + 2 + 5 = 10, which is the maximum Alice can get.
Example 2:
Input: piles = [4,3,2,5,10]
Output: 11Constraints:
1 <= piles.length <= 1001 <= piles[i] <= 10,000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming (Memoization) - Caching optimal results for each game state to avoid redundant computation
- Game Theory / Minimax - Understanding two-player optimal play where one player maximizes and the other minimizes
- Suffix Sum - Precomputing cumulative sums from right to left to efficiently calculate remaining stones
- Multi-dimensional State Tracking - Managing DP states with multiple parameters (position, M value, whose turn)
1. Dynamic Programming (Top-Down)
Intuition
This is a two-player game where Alice and Bob take turns picking piles from the front. The key insight is that both players play optimally, meaning Alice tries to maximize her score while Bob tries to minimize Alice's score. We can model this using recursion with memoization, tracking whose turn it is, the current index, and the value of M. When it's Alice's turn, she picks the option that maximizes her total; when it's Bob's turn, he picks the option that minimizes Alice's total.
Algorithm
- Use a recursive function
dfs(alice, i, M)wherealiceindicates whose turn it is,iis the current pile index, andMis the maximum number of piles that can be taken. - Base case: if
i == n, return0(no more piles). - If it's Alice's turn, try taking
Xpiles (1 to2M) and maximize the result by adding the stones taken plus the recursive call for Bob's turn. - If it's Bob's turn, try taking
Xpiles and minimize the result since Bob wants to minimize Alice's score. - Update
Mtomax(M, X)after each move. - Memoize results using a 3D cache with dimensions
[2][n][n+1]. - Return
dfs(True, 0, 1)to get Alice's maximum score.
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
dp = {}
def dfs(alice, i, M):
if i == len(piles):
return 0
if (alice, i, M) in dp:
return dp[(alice, i, M)]
res = 0 if alice else float("inf")
total = 0
for X in range(1, 2 * M + 1):
if i + X > len(piles):
break
total += piles[i + X - 1]
if alice:
res = max(res, total + dfs(not alice, i + X, max(M, X)))
else:
res = min(res, dfs(not alice, i + X, max(M, X)))
dp[(alice, i, M)] = res
return res
return dfs(True, 0, 1)Time & Space Complexity
- Time complexity:
- Space complexity:
2. Dynamic Programming (Top-Down) + Suffix Sum
Intuition
Instead of tracking both players separately, we can simplify by focusing on a single player's perspective. At each position, the current player wants to maximize their own score. The trick is that whatever stones remain after the current player's turn will be split optimally by the opponent. Using suffix sums, if the current player takes some stones, their score equals those stones plus whatever remains minus the opponent's optimal result from the remaining position.
Algorithm
- Precompute suffix sums where
suffix_sum[i]is the total stones from indexito the end. - Define
dfs(i, M)to return the maximum stones the current player can get starting at indexiwith parameterM. - Base case: if
i == n, return0. - For each choice
Xfrom1to2M, the current player can takesuffix_sum[i] - dfs(i + X, max(M, X)). This works because the current player gets all remaining stones minus what the opponent will optimally take. - Memoize with a 2D cache of size
[n][n+1]. - Return
dfs(0, 1)to get Alice's maximum score.
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
n = len(piles)
dp = [[None] * (n + 1) for _ in range(n)]
suffix_sum = [0] * n
suffix_sum[-1] = piles[-1]
for i in range(n - 2, -1, -1):
suffix_sum[i] = piles[i] + suffix_sum[i + 1]
def dfs(i, M):
if i == n:
return 0
if dp[i][M] is not None:
return dp[i][M]
res = 0
for X in range(1, 2 * M + 1):
if i + X > n:
break
res = max(res, suffix_sum[i] - dfs(i + X, max(M, X)))
dp[i][M] = res
return res
return dfs(0, 1)Time & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
We can convert the top-down approach to bottom-up by filling the DP table from the end of the piles array backward. At each position, we compute the optimal result for both Alice and Bob, considering all possible moves. Working backward ensures that when we compute a state, all states it depends on have already been computed.
Algorithm
- Create a 3D DP array
dp[2][n+1][n+1]where the first dimension represents the player (1 for Alice, 0 for Bob). - Iterate from
i = n-1down to0, and for eachMfrom1ton. - For Alice's turn, initialize
dp[1][i][M] = 0and try takingXpiles (1 to2M), computingtotal + dp[0][i+X][max(M, X)]and taking the maximum. - For Bob's turn, initialize
dp[0][i][M] = infinityand try takingXpiles, computingdp[1][i+X][max(M, X)]and taking the minimum. - Return
dp[1][0][1]for Alice's maximum score starting at position0withM=1.
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
n = len(piles)
dp = [[[0] * (n + 1) for _ in range(n + 1)] for _ in range(2)]
for i in range(n - 1, -1, -1):
for M in range(1, n + 1):
total = 0
dp[1][i][M] = 0
dp[0][i][M] = float("inf")
for X in range(1, 2 * M + 1):
if i + X > n:
break
total += piles[i + X - 1]
dp[1][i][M] = max(dp[1][i][M], total + dp[0][i + X][max(M, X)])
dp[0][i][M] = min(dp[0][i][M], dp[1][i + X][max(M, X)])
return dp[1][0][1]Time & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Bottom-Up) + Suffix Sum
Intuition
Combining the bottom-up approach with the suffix sum optimization gives us a cleaner solution. Since we treat both players symmetrically (each maximizes their own score), we only need a 2D DP table. The suffix sum lets us compute how many stones the current player gets by subtracting the opponent's optimal result from the remaining total.
Algorithm
- Precompute suffix sums where
suffix_sum[i]is the total stones from indexito the end. - Create a 2D DP array
dp[n+1][n+1]initialized to0. - Iterate from
i = n-1down to0, and for eachMfrom1ton. - For each
Xfrom1to2M(whilei + X <= n), computedp[i][M] = max(dp[i][M], suffix_sum[i] - dp[i+X][max(M, X)]). - Return
dp[0][1]as Alice's maximum score.
class Solution:
def stoneGameII(self, piles: List[int]) -> int:
n = len(piles)
suffix_sum = [0] * n
suffix_sum[-1] = piles[-1]
for i in range(n - 2, -1, -1):
suffix_sum[i] = piles[i] + suffix_sum[i + 1]
dp = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(n - 1, -1, -1):
for M in range(1, n + 1):
for X in range(1, 2 * M + 1):
if i + X > n:
break
dp[i][M] = max(dp[i][M], suffix_sum[i] - dp[i + X][max(M, X)])
return dp[0][1]Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Misunderstanding the M Parameter Update
A frequent error is updating M incorrectly after a move. The rule is M = max(M, X) where X is the number of piles taken, not M = X or M = M + X. Failing to take the maximum means M might decrease, which violates the game rules and leads to wrong answers.
Confusing Whose Score to Track
Since both players play optimally but only Alice's score matters for the answer, it is easy to mix up the logic. When using the minimax approach, Alice maximizes her score while Bob minimizes it. When using the suffix sum trick, both players maximize their own score, and the recursion naturally handles the alternation.
Off-by-One Errors in Loop Bounds
The player can take X piles where 1 <= X <= 2*M. A common mistake is iterating X from 0 to 2*M or from 1 to 2*M - 1. Ensure your loop correctly covers exactly the valid range of moves, and check that i + X does not exceed the array length before accessing elements.
Sign in to join the discussion