leetcode1563. 石子游戏 V
几块石子 排成一行 ,每块石子都有一个关联值,关联值为整数,由数组 stoneValue
给出。
游戏中的每一轮:Alice 会将这行石子分成两个 非空行(即,左侧行和右侧行);Bob 负责计算每一行的值,即此行中所有石子的值的总和。Bob 会丢弃值最大的行,Alice 的得分为剩下那行的值(每轮累加)。如果两行的值相等,Bob 让 Alice 决定丢弃哪一行。下一轮从剩下的那一行开始。
只 剩下一块石子 时,游戏结束。Alice 的分数最初为 0
。
返回 Alice 能够获得的最大分数 。
示例 1:
输入:stoneValue = [6,2,3,4,5,5]
输出:18
解释:在第一轮中,Alice 将行划分为 [6,2,3],[4,5,5] 。左行的值是 11 ,右行的值是 14 。Bob 丢弃了右行,Alice 的分数现在是 11 。
在第二轮中,Alice 将行分成 [6],[2,3] 。这一次 Bob 扔掉了左行,Alice 的分数变成了 16(11 + 5)。
最后一轮 Alice 只能将行分成 [2],[3] 。Bob 扔掉右行,Alice 的分数现在是 18(16 + 2)。游戏结束,因为这行只剩下一块石头了。
示例 2:
输入:stoneValue = [7,7,7,7,7,7,7]
输出:28
示例 3:
输入:stoneValue = [4]
输出:0
提示:
1 <= stoneValue.length <= 500
1 <= stoneValue[i] <= 10^6
方法:动态规划(递归+记忆化)
思路:
本题很明显应该使用动态规划的方法。
首先,我们看状态的表示,使用f(i,j)表示对石头数组的子数组stoneValue[i:j+1]可以得到的最大分数。那么我们最后的答案应该为f(0,n-1),即对整个stoneValue数组进行游戏。
下面我们考虑状态的转移。对于f(i,j)进行游戏,我们对这个子数组可以有多种分法(分为左右区间),**我们假设以下标k为左区间的右端点。**那么,就可以分为(i,k)和(k+1,j)两组,这里k的取值为[i,j],那么一共有j-i+1中分法。
我们考虑k的时候,如何进行计算,首先计算出左右两个区间的和,设为left和right,那么根据游戏的规则,有以下几种情况:
- 如果left>right,那么左边的和大,根据规则,抛弃左边,则分数为right+对右边进行递归,即f(k+1,j)
- 如果left<right,那么右边的和大,则抛弃右边,分数为left+f(i,k)
- 如果left和right相等,则需要考虑对两边分别递归,哪个大,因此分数为left + max(f(i,k),f(k+1,j))
在将所有的k遍历之后,最大的分数即为f(i,j)。
对于计算区间的和,我们可以使用presum表示前缀和数组。presum[i]表示前i个数的和(即下标0到i-1的值),那么假设区间为[i,j](闭区间),则这个区间的和为presum[j+1]-presum[i]。
对于动态规划,我们可以递归+记忆化,也可以使用dp数组迭代。对于迭代,时间复杂度为O(N ^ 3),可能会超时,因此使用递归+记忆化来减少一些无意义的计算,详细见代码。
代码:
Python3:
class Solution:
def stoneGameV(self, stoneValue: List[int]) -> int:
n = len(stoneValue)
# 前缀和数组,那么[i,j]的和为presum[j+1]-presum[i]
presum = [0]*(n+1)
for i in range(1,n+1):
presum[i] = stoneValue[i-1] + presum[i-1]
@lru_cache(None)
def dfs(l,r):
res = 0
# 只剩一个石子,得分是0
if l == r:
return res
# 遍历所有可能的分割位置
for k in range(l,r+1):
# left和right表示左右区间的和
left = presum[k+1]-presum[l]
right = presum[r+1]-presum[k+1]
# 左边和大于右边和,那么丢弃左边,分数为右边和+对右边递归的结果
if left > right:
res = max(res,right+dfs(k+1,r))
# 右边和大于左边和,那么丢弃右边,分数为左边和+对左边递归的结果
elif left < right:
res = max(res,left+dfs(l,k))
# 二者相等,分数为和+左右两边递归结果中较大的
else:
res = max(res,left+max(dfs(l,k),dfs(k+1,r)))
# 遍历了所有的k,返回最大的res
return res
return dfs(0,n-1)
cpp:
class Solution {
public:
// memo为记忆化备忘录,presum为前缀和,那么[i,j]的和为presum[j+1]-presum[i]
int memo[501][501];
int presum[501];
int stoneGameV(vector<int>& stoneValue) {
// 填写前缀和数组
int n = stoneValue.size();
for (int i = 1; i <= n; i++) presum[i] = stoneValue[i-1] + presum[i-1];
// 递归计算答案
return dfs(0,n-1);
}
// 递归函数
int dfs(int l,int r){
// 如果之前求过这一对值,直接返回
if (memo[l][r])
return memo[l][r];
// res保存答案
int res = 0;
// 如果长度为1,那么得分为0
if (l == r) return res;
// 使用k分成左右两组,遍历所有可能的分组k
for (int k = l; k <= r; k++){
// 表示左右区间的和
int left = presum[k+1] - presum[l];
int right = presum[r+1] - presum[k+1];
// 左边和更大,此时分数为right+对右边递归求值
if (left > right)
res = max(res,right+dfs(k+1,r));
// 右边和更大,此时分数为left+对左边递归求值
else if(left < right)
res = max(res,left+dfs(l,k));
// 左右一样大,此时分数为这个和+对左右分别递归求值的较大值
else
res = max(res,left+max(dfs(l,k),dfs(k+1,r)));
}
// 遍历所有k的最大res即为答案,更新memo中,返回res
memo[l][r] = res;
return res;
}
};