Facebook Pixel

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:

  1. Alice's turn: She divides the current row of stones into two non-empty parts (a left row and a right row)
  2. Bob's turn: He calculates the sum of values in each row, then discards the row with the maximum sum
  3. Alice's score: The sum of the remaining row is added to Alice's score
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 adding stoneValue[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)
  • 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)
  • If l == r: Alice chooses which part to keep

    • Score = max(l + dfs(i, k), r + dfs(k+1, j))

6. Pruning Logic: The pruning works because:

  • When keeping a part with sum x, the maximum possible score is x (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 as k 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 Evaluator

Example 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 sum r = 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 sum r = 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 and 2 * 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 sum r = 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) where 0 ≤ 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 points k from i to j-1, which takes O(j - i) = O(n) time in the worst case.
  • Within each iteration, operations like updating l and r, comparisons, and recursive calls (which return cached results) take O(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 takes O(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 if max_score >= 2 * left_sum
  • When left_sum > right_sum: Break early if max_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) and dp(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)))
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More