1563. Stone Game V
Problem Description
You are given an array stoneValue
where each element represents the value of a stone arranged in a row. Alice and Bob play a game with these stones.
The game proceeds in rounds:
- Alice's turn: She divides the current row of stones into two non-empty parts (a left row and a right row)
- Bob's turn: He calculates the sum of values in each row, then discards the row with the maximum sum
- Alice's score: The sum of the remaining row is added to Alice's score
- Special case: If both rows have equal sums, Bob lets Alice choose which row to discard
The game continues with the remaining row until only one stone is left (at which point no more divisions are possible). Alice starts with a score of 0.
Your task is to find the maximum score Alice can achieve if she plays optimally.
Example walkthrough:
If stoneValue = [6, 2, 3, 4, 5, 5]
:
- Alice could split into
[6, 2, 3]
(sum = 11) and[4, 5, 5]
(sum = 14) - Bob discards
[4, 5, 5]
since 14 > 11 - Alice gets 11 points and continues with
[6, 2, 3]
- The process repeats until one stone remains
The goal is to determine Alice's optimal splitting strategy to maximize her total score across all rounds.
Intuition
The key insight is that this is a dynamic programming problem where Alice wants to maximize her score by choosing optimal split points. Since Alice controls how to split the stones and wants to maximize her score, she needs to consider all possible ways to divide the current row.
For any subarray of stones, Alice needs to try every possible split point. When she splits at position k
, she creates two parts: left [i, k]
and right [k+1, j]
. Bob will discard the part with the larger sum, so Alice gets the smaller sum added to her score. Then the game continues recursively with the remaining part.
The recursive nature becomes clear: if Alice keeps the left part (because it has smaller or equal sum), she gets left_sum + dfs(i, k)
total score. If she keeps the right part, she gets right_sum + dfs(k+1, j)
total score. When both parts have equal sums, Alice can choose which one to keep, so she picks the option that gives her the maximum future score.
To optimize this recursive approach, we can use memoization to avoid recalculating the same subproblems. The state is defined by the boundaries (i, j)
of the current subarray.
The pruning optimization comes from recognizing that if we've already found a good score ans
, and the current split can at most give us 2 * smaller_sum
(the smaller sum now plus at most the same amount in future rounds), we can skip this split if ans >= 2 * smaller_sum
. This is because even in the best case, this split won't improve our answer.
Using prefix sums s
allows us to quickly calculate the sum of any subarray: sum[i to j] = s[j+1] - s[i]
, which makes the implementation more efficient.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with memoization and pruning optimizations. Here's how the implementation works:
1. Prefix Sum Preprocessing:
First, we build a prefix sum array s
where s[i]
represents the sum of the first i
elements of stoneValue
. This allows us to calculate the sum of any subarray in O(1) time: sum[i to j] = s[j+1] - s[i]
.
2. Recursive Function with Memoization:
We define dfs(i, j)
to represent the maximum score Alice can obtain from stones in the range [i, j]
. The @cache
decorator provides automatic memoization to avoid redundant calculations.
3. Base Case:
If i >= j
, there's only one stone (or no stones) left, so Alice can't make any splits and returns 0.
4. Enumeration of Split Points:
For each possible split point k
where i ≤ k < j
, we:
- Calculate
l
(left sum) incrementally by addingstoneValue[k]
- Calculate
r
(right sum) by subtracting from the total sum of the range
5. Decision Making Based on Sums:
-
If
l < r
: Bob discards the right part, Alice keeps the left part- Score =
l + dfs(i, k)
- Pruning: Skip if
ans >= l * 2
(current answer already better than best possible)
- Score =
-
If
l > r
: Bob discards the left part, Alice keeps the right part- Score =
r + dfs(k+1, j)
- Pruning: Break the loop if
ans >= r * 2
(no future splits can improve)
- Score =
-
If
l == r
: Alice chooses which part to keep- Score =
max(l + dfs(i, k), r + dfs(k+1, j))
- Score =
6. Pruning Logic: The pruning works because:
- When keeping a part with sum
x
, the maximum possible score isx
(current) +x
(best case future) =2x
- If our current best
ans >= 2x
, this split can't improve our answer - For the
l > r
case, we can break early since ask
increases,r
only gets smaller
7. Final Answer:
The function returns dfs(0, len(stoneValue) - 1)
, which represents the maximum score for the entire array.
The time complexity is O(n³) with memoization (n² states, each taking O(n) to compute), and space complexity is O(n²) for 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 stoneValue = [7, 3, 4, 5]
to illustrate how the solution works.
Initial Setup:
- Prefix sum array:
s = [0, 7, 10, 14, 19]
- We call
dfs(0, 3)
to find the maximum score for the entire array
Round 1: dfs(0, 3)
- stones [7, 3, 4, 5]
We try each possible split point:
Split at k=0: [7] | [3, 4, 5]
- Left sum
l = 7
, Right sumr = 12
- Since
l < r
, Bob discards right part, Alice keeps left - Score = 7 +
dfs(0, 0)
= 7 + 0 = 7 - Update
ans = 7
Split at k=1: [7, 3] | [4, 5]
- Left sum
l = 10
, Right sumr = 9
- Since
l > r
, Bob discards left part, Alice keeps right - Score = 9 +
dfs(2, 3)
= 9 + ? (need to compute) - First, check pruning: Since
ans = 7
and2 * r = 18
, no pruning
Compute dfs(2, 3)
- stones [4, 5]:
- Only one split possible at k=2: [4] | [5]
- Left sum = 4, Right sum = 5
- Since 4 < 5, Alice keeps left part
- Score = 4 +
dfs(2, 2)
= 4 + 0 = 4 - Return 4
Back to Split at k=1:
- Score = 9 + 4 = 13
- Update
ans = 13
Split at k=2: [7, 3, 4] | [5]
- Left sum
l = 14
, Right sumr = 5
- Since
l > r
, Bob discards left part, Alice keeps right - Score = 5 +
dfs(3, 3)
= 5 + 0 = 5 ans
remains 13 (no improvement)- Pruning check: Since
ans = 13 >= 2 * 5 = 10
, we could break here
Final Result: dfs(0, 3)
returns 13
Optimal Strategy: Alice's optimal play is to split [7, 3] | [4, 5] in the first round:
- Bob discards [7, 3] (sum = 10)
- Alice scores 9 points and continues with [4, 5]
- Next, she splits [4] | [5]
- Bob discards [5]
- Alice scores 4 more points
- Total score: 9 + 4 = 13
The memoization ensures that if we encounter the same subarray again (e.g., dfs(2, 3)
), we don't recalculate it. The pruning optimization helps skip splits that can't possibly improve our current best answer, making the algorithm more efficient.
Solution Implementation
1from typing import List
2from functools import cache
3from itertools import accumulate
4
5
6class Solution:
7 def stoneGameV(self, stoneValue: List[int]) -> int:
8 """
9 Stone Game V: Alice and Bob play a game with stones in a row.
10 Each turn, a player divides stones into two non-empty groups.
11 The player with smaller sum gets points equal to that sum.
12 If sums are equal, player chooses which sum to take as points.
13 Returns maximum points Alice can get with optimal play.
14 """
15
16 @cache
17 def dp(left: int, right: int) -> int:
18 """
19 Dynamic programming function to find maximum score in range [left, right].
20
21 Args:
22 left: Starting index of the range (inclusive)
23 right: Ending index of the range (inclusive)
24
25 Returns:
26 Maximum score achievable from stones in range [left, right]
27 """
28 # Base case: if range is invalid or contains single stone, no score
29 if left >= right:
30 return 0
31
32 max_score = 0
33 left_sum = 0
34 # Calculate right sum using prefix sum array
35 right_sum = prefix_sum[right + 1] - prefix_sum[left]
36
37 # Try all possible split points
38 for split_point in range(left, right):
39 # Update sums after including current stone in left group
40 left_sum += stoneValue[split_point]
41 right_sum -= stoneValue[split_point]
42
43 if left_sum < right_sum:
44 # Left group has smaller sum, Alice gets left_sum points
45 # Pruning: skip if current max_score is already >= 2 * left_sum
46 if max_score >= left_sum * 2:
47 continue
48 max_score = max(max_score, left_sum + dp(left, split_point))
49
50 elif left_sum > right_sum:
51 # Right group has smaller sum, Alice gets right_sum points
52 # Pruning: break early if max_score is already >= 2 * right_sum
53 if max_score >= right_sum * 2:
54 break
55 max_score = max(max_score, right_sum + dp(split_point + 1, right))
56
57 else:
58 # Both groups have equal sum, Alice chooses the better option
59 max_score = max(
60 max_score,
61 max(left_sum + dp(left, split_point),
62 right_sum + dp(split_point + 1, right))
63 )
64
65 return max_score
66
67 # Build prefix sum array for efficient range sum queries
68 # prefix_sum[i] = sum of stoneValue[0:i]
69 prefix_sum = list(accumulate(stoneValue, initial=0))
70
71 # Start the recursive solution from the entire array
72 return dp(0, len(stoneValue) - 1)
73
1class Solution {
2 private int n; // Total number of stones
3 private int[] prefixSum; // Prefix sum array for quick range sum calculation
4 private int[] stoneValues; // Original stone values array
5 private Integer[][] memo; // Memoization table for dynamic programming
6
7 public int stoneGameV(int[] stoneValue) {
8 // Initialize variables
9 n = stoneValue.length;
10 prefixSum = new int[n + 1];
11 stoneValues = stoneValue;
12 memo = new Integer[n][n];
13
14 // Build prefix sum array for O(1) range sum queries
15 // prefixSum[i] represents sum of elements from index 0 to i-1
16 for (int i = 1; i <= n; i++) {
17 prefixSum[i] = prefixSum[i - 1] + stoneValues[i - 1];
18 }
19
20 // Start the recursive solution with memoization
21 return dfs(0, n - 1);
22 }
23
24 private int dfs(int left, int right) {
25 // Base case: if range is invalid or contains only one element
26 if (left >= right) {
27 return 0;
28 }
29
30 // Check if result is already computed
31 if (memo[left][right] != null) {
32 return memo[left][right];
33 }
34
35 int maxScore = 0;
36 int leftSum = 0; // Sum of left partition
37 int rightSum = prefixSum[right + 1] - prefixSum[left]; // Sum of right partition
38
39 // Try all possible split points
40 for (int splitPoint = left; splitPoint < right; splitPoint++) {
41 // Update partition sums
42 leftSum += stoneValues[splitPoint];
43 rightSum -= stoneValues[splitPoint];
44
45 if (leftSum < rightSum) {
46 // Left partition has smaller sum, Alice chooses left
47 // Pruning: if current answer is already greater than maximum possible score
48 if (maxScore > leftSum * 2) {
49 continue;
50 }
51 maxScore = Math.max(maxScore, leftSum + dfs(left, splitPoint));
52 } else if (leftSum > rightSum) {
53 // Right partition has smaller sum, Alice chooses right
54 // Pruning: if current answer is already greater than maximum possible score
55 if (maxScore > rightSum * 2) {
56 break;
57 }
58 maxScore = Math.max(maxScore, rightSum + dfs(splitPoint + 1, right));
59 } else {
60 // Both partitions have equal sum, Alice can choose either
61 maxScore = Math.max(maxScore,
62 Math.max(leftSum + dfs(left, splitPoint),
63 rightSum + dfs(splitPoint + 1, right)));
64 }
65 }
66
67 // Store result in memo table and return
68 memo[left][right] = maxScore;
69 return maxScore;
70 }
71}
72
1class Solution {
2public:
3 int stoneGameV(vector<int>& stoneValue) {
4 int n = stoneValue.size();
5
6 // Build prefix sum array for quick range sum calculation
7 // prefixSum[i] = sum of stoneValue[0] to stoneValue[i-1]
8 vector<int> prefixSum(n + 1, 0);
9 for (int i = 0; i < n; i++) {
10 prefixSum[i + 1] = prefixSum[i] + stoneValue[i];
11 }
12
13 // Memoization table for dynamic programming
14 // dp[i][j] = maximum score Alice can get from stones[i..j]
15 vector<vector<int>> dp(n, vector<int>(n, -1));
16
17 // Recursive function with memoization to find maximum score
18 auto dfs = [&](this auto&& dfs, int left, int right) -> int {
19 // Base case: if range is invalid or has only one stone
20 if (left >= right) {
21 return 0;
22 }
23
24 // Return memoized result if already computed
25 if (dp[left][right] != -1) {
26 return dp[left][right];
27 }
28
29 int maxScore = 0;
30 int leftSum = 0;
31 int rightSum = prefixSum[right + 1] - prefixSum[left];
32
33 // Try all possible split points
34 for (int splitPoint = left; splitPoint < right; ++splitPoint) {
35 leftSum += stoneValue[splitPoint];
36 rightSum -= stoneValue[splitPoint];
37
38 if (leftSum < rightSum) {
39 // Bob chooses right part, Alice gets left part's sum
40 // Pruning: if current answer is already greater than maximum possible score
41 if (maxScore > leftSum * 2) {
42 continue;
43 }
44 maxScore = max(maxScore, leftSum + dfs(left, splitPoint));
45 }
46 else if (leftSum > rightSum) {
47 // Bob chooses left part, Alice gets right part's sum
48 // Pruning: if current answer is already greater than maximum possible score
49 if (maxScore > rightSum * 2) {
50 break;
51 }
52 maxScore = max(maxScore, rightSum + dfs(splitPoint + 1, right));
53 }
54 else {
55 // Left sum equals right sum, Bob can choose either part
56 // Alice gets the sum plus the optimal score from chosen part
57 maxScore = max({maxScore,
58 leftSum + dfs(left, splitPoint),
59 rightSum + dfs(splitPoint + 1, right)});
60 }
61 }
62
63 // Store and return the result
64 dp[left][right] = maxScore;
65 return maxScore;
66 };
67
68 // Start the game with all stones (indices 0 to n-1)
69 return dfs(0, n - 1);
70 }
71};
72
1/**
2 * Solves the Stone Game V problem using dynamic programming with memoization.
3 * The goal is to find the maximum score Alice can obtain by splitting stones optimally.
4 *
5 * @param stoneValue - Array of stone values
6 * @returns Maximum score Alice can obtain
7 */
8function stoneGameV(stoneValue: number[]): number {
9 const n: number = stoneValue.length;
10
11 // Prefix sum array for quick range sum calculation
12 // prefixSum[i] represents sum of stones from index 0 to i-1
13 const prefixSum: number[] = Array(n + 1).fill(0);
14 for (let i = 0; i < n; i++) {
15 prefixSum[i + 1] = prefixSum[i] + stoneValue[i];
16 }
17
18 // Memoization table for dynamic programming
19 // memo[i][j] stores the maximum score for subarray from index i to j
20 const memo: number[][] = Array.from({ length: n }, () => Array(n).fill(-1));
21
22 /**
23 * Recursive function with memoization to find maximum score for a range.
24 *
25 * @param left - Starting index of the range (inclusive)
26 * @param right - Ending index of the range (inclusive)
27 * @returns Maximum score obtainable from the given range
28 */
29 const dfs = (left: number, right: number): number => {
30 // Base case: single stone or invalid range
31 if (left >= right) {
32 return 0;
33 }
34
35 // Check if result is already computed
36 if (memo[left][right] !== -1) {
37 return memo[left][right];
38 }
39
40 let maxScore: number = 0;
41 let leftSum: number = 0;
42 let rightSum: number = prefixSum[right + 1] - prefixSum[left];
43
44 // Try all possible split positions
45 for (let splitPos = left; splitPos < right; splitPos++) {
46 leftSum += stoneValue[splitPos];
47 rightSum -= stoneValue[splitPos];
48
49 if (leftSum < rightSum) {
50 // Left part is smaller, Alice takes left and continues with left range
51 // Pruning: skip if current max score is already greater than twice the left sum
52 if (maxScore > leftSum * 2) {
53 continue;
54 }
55 maxScore = Math.max(maxScore, leftSum + dfs(left, splitPos));
56 } else if (leftSum > rightSum) {
57 // Right part is smaller, Alice takes right and continues with right range
58 // Pruning: break early if current max score is already greater than twice the right sum
59 if (maxScore > rightSum * 2) {
60 break;
61 }
62 maxScore = Math.max(maxScore, rightSum + dfs(splitPos + 1, right));
63 } else {
64 // Both parts are equal, Alice can choose either
65 maxScore = Math.max(
66 maxScore,
67 leftSum + dfs(left, splitPos),
68 rightSum + dfs(splitPos + 1, right)
69 );
70 }
71 }
72
73 // Store and return the computed result
74 memo[left][right] = maxScore;
75 return maxScore;
76 };
77
78 // Start the recursive computation for the entire array
79 return dfs(0, n - 1);
80}
81
Time and Space Complexity
Time Complexity: O(n³)
The function uses dynamic programming with memoization through the @cache
decorator. The dfs
function explores all possible split points for subarrays defined by indices (i, j)
.
- There are
O(n²)
possible unique states(i, j)
where0 ≤ i < j < n
, representing all possible subarrays. - Due to memoization, each state is computed only once.
- For each state
(i, j)
, the function iterates through all possible split pointsk
fromi
toj-1
, which takesO(j - i) = O(n)
time in the worst case. - Within each iteration, operations like updating
l
andr
, comparisons, and recursive calls (which return cached results) takeO(1)
time.
Therefore, the total time complexity is O(n²) × O(n) = O(n³)
.
Space Complexity: O(n²)
The space complexity consists of:
- The memoization cache stores results for all
O(n²)
possible states(i, j)
. - The recursion stack depth is at most
O(n)
in the worst case when the array is split into increasingly smaller subarrays. - The prefix sum array
s
takesO(n)
space.
Since O(n²)
dominates, the overall space complexity is O(n²)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Sum Calculation Without Prefix Sum
A common mistake is recalculating the sum of subarrays repeatedly within the recursive function, leading to O(n⁴) time complexity instead of O(n³).
Pitfall Example:
def dp(left, right):
# Wrong: Recalculating sum every time
for split_point in range(left, right):
left_sum = sum(stoneValue[left:split_point+1]) # O(n) operation
right_sum = sum(stoneValue[split_point+1:right+1]) # O(n) operation
Solution: Use prefix sums for O(1) range sum queries, or maintain running sums incrementally as shown in the correct implementation.
2. Missing or Incorrect Pruning Logic
Without proper pruning, the solution may time out on larger inputs. The key insight is that if we already have a score ans
, and the current partition gives us sum x
, the maximum we can possibly achieve is x + x = 2x
(current score + best possible future score).
Pitfall Example:
# Missing pruning entirely
for split_point in range(left, right):
if left_sum < right_sum:
max_score = max(max_score, left_sum + dp(left, split_point))
# No early termination or skip conditions
Solution:
- When
left_sum < right_sum
: Skip ifmax_score >= 2 * left_sum
- When
left_sum > right_sum
: Break early ifmax_score >= 2 * right_sum
(since right_sum only decreases)
3. Confusion About Index Boundaries
The recursive function uses inclusive boundaries [left, right]
, but it's easy to mix this up with exclusive boundaries or off-by-one errors.
Pitfall Example:
# Wrong: Treating right as exclusive
def dp(left, right):
if left >= right - 1: # Wrong base case
return 0
for split_point in range(left, right - 1): # Wrong range
Solution: Be consistent with inclusive boundaries:
- Base case:
if left >= right
(single stone or invalid range) - Split range:
for split_point in range(left, right)
- Recursive calls:
dp(left, split_point)
anddp(split_point + 1, right)
4. Misunderstanding the Equal Sum Case
When both partitions have equal sums, Alice gets to choose which partition to keep. A common mistake is to always pick one side or to add both scores.
Pitfall Example:
if left_sum == right_sum:
# Wrong: Adding both scores
max_score = max(max_score, left_sum + dp(left, split_point) +
right_sum + dp(split_point + 1, right))
# Wrong: Always choosing left
max_score = max(max_score, left_sum + dp(left, split_point))
Solution: Alice chooses the partition that maximizes her total score:
max_score = max(max_score,
max(left_sum + dp(left, split_point),
right_sum + dp(split_point + 1, right)))
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
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!