In the Stone Game V problem, you are given an array of positive integers called stoneValue
, where each element represents the value of a stone in a row. Alice and Bob play a game with the following rules:
i
(where left = [stoneValue[0], ..., stoneValue[i]]
and right = [stoneValue[i+1], ..., stoneValue[n-1]]
).The goal is to determine the maximum score Alice can obtain by playing optimally from the entire row.
Constraints:
At first glance, the problem appears to involve trying every possible way to split the array at each stage, recursively, and keeping track of the maximum score Alice can achieve. Since Alice can always choose the optimal path, we need to consider all possible splits and, at each step, simulate the game's rules.
A brute-force approach would recursively try every split, but this leads to a huge number of repeated calculations, especially as the array grows. This is a classic case for dynamic programming (DP), where overlapping subproblems can be solved efficiently by storing and reusing results.
The main challenge is:
We use a top-down dynamic programming approach with memoization:
prefix[i]
is the sum of stoneValue[0]
to stoneValue[i-1]
.dp(l, r)
as the maximum score Alice can achieve from subarray stoneValue[l:r+1]
.i
between l
and r
:
leftSum = sum(l, i)
, rightSum = sum(i+1, r)
.leftSum < rightSum
, Alice continues on left and adds leftSum
to her score.rightSum < leftSum
, Alice continues on right and adds rightSum
to her score.(l, r)
range.l == r
, there's only one stone left, so the score is 0.dp(0, n-1)
where n
is the length of stoneValue
.This approach ensures that each subproblem is solved only once, making the solution efficient even for the upper constraint of 500 stones.
Let's walk through an example with stoneValue = [6,2,3,4,5,5]
:
dp(0,5)
(full array).
dp(0,2)
or dp(4,5)
are only computed once.
Brute-force approach:
For n = 500, this is efficient enough due to modern computation power and the use of memoization.
The Stone Game V problem is a classic example of using dynamic programming to optimize a recursive process with overlapping subproblems. By precomputing prefix sums and using memoization, we avoid redundant work and efficiently compute the maximum score Alice can obtain by always playing optimally. The approach is both systematic and scalable, turning an otherwise intractable brute-force solution into an elegant and performant one.
from functools import lru_cache
class Solution:
def stoneGameV(self, stoneValue):
n = len(stoneValue)
prefix = [0] * (n + 1)
for i in range(n):
prefix[i+1] = prefix[i] + stoneValue[i]
@lru_cache(None)
def dp(l, r):
if l == r:
return 0
res = 0
for i in range(l, r):
left = prefix[i+1] - prefix[l]
right = prefix[r+1] - prefix[i+1]
if left < right:
res = max(res, left + dp(l, i))
elif left > right:
res = max(res, right + dp(i+1, r))
else:
res = max(res, left + dp(l, i), right + dp(i+1, r))
return res
return dp(0, n-1)
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
class Solution {
public:
int stoneGameV(vector<int>& stoneValue) {
int n = stoneValue.size();
vector<int> prefix(n+1, 0);
for (int i = 0; i < n; ++i)
prefix[i+1] = prefix[i] + stoneValue[i];
vector<vector<int>> dp(n, vector<int>(n, -1));
function<int(int, int)> solve = [&](int l, int r) {
if (l == r) return 0;
if (dp[l][r] != -1) return dp[l][r];
int res = 0;
for (int i = l; i < r; ++i) {
int left = prefix[i+1] - prefix[l];
int right = prefix[r+1] - prefix[i+1];
if (left < right)
res = max(res, left + solve(l, i));
else if (left > right)
res = max(res, right + solve(i+1, r));
else
res = max({res, left + solve(l, i), right + solve(i+1, r)});
}
return dp[l][r] = res;
};
return solve(0, n-1);
}
};
import java.util.*;
class Solution {
public int stoneGameV(int[] stoneValue) {
int n = stoneValue.length;
int[] prefix = new int[n+1];
for (int i = 0; i < n; ++i)
prefix[i+1] = prefix[i] + stoneValue[i];
int[][] dp = new int[n][n];
for (int[] row : dp)
Arrays.fill(row, -1);
return dfs(0, n-1, prefix, dp);
}
private int dfs(int l, int r, int[] prefix, int[][] dp) {
if (l == r) return 0;
if (dp[l][r] != -1) return dp[l][r];
int res = 0;
for (int i = l; i < r; ++i) {
int left = prefix[i+1] - prefix[l];
int right = prefix[r+1] - prefix[i+1];
if (left < right)
res = Math.max(res, left + dfs(l, i, prefix, dp));
else if (left > right)
res = Math.max(res, right + dfs(i+1, r, prefix, dp));
else
res = Math.max(res, Math.max(left + dfs(l, i, prefix, dp), right + dfs(i+1, r, prefix, dp)));
}
dp[l][r] = res;
return res;
}
}
var stoneGameV = function(stoneValue) {
const n = stoneValue.length;
const prefix = new Array(n+1).fill(0);
for (let i = 0; i < n; ++i)
prefix[i+1] = prefix[i] + stoneValue[i];
const dp = Array.from({length: n}, () => Array(n).fill(-1));
function dfs(l, r) {
if (l === r) return 0;
if (dp[l][r] !== -1) return dp[l][r];
let res = 0;
for (let i = l; i < r; ++i) {
let left = prefix[i+1] - prefix[l];
let right = prefix[r+1] - prefix[i+1];
if (left < right)
res = Math.max(res, left + dfs(l, i));
else if (left > right)
res = Math.max(res, right + dfs(i+1, r));
else
res = Math.max(res, left + dfs(l, i), right + dfs(i+1, r));
}
dp[l][r] = res;
return res;
}
return dfs(0, n-1);
};