【数据结构与算法】之深入解析“石子游戏VIII”的求解思路与算法示例
【摘要】
一、题目描述
Alice 和 Bob 玩一个游戏,两人轮流操作, Alice 先手,总共有 n 个石子排成一行。轮到某个玩家的回合时,如果石子的数目 大于 1 ,他将执行以下操作:
选择一个整数 ...
一、题目描述
- Alice 和 Bob 玩一个游戏,两人轮流操作, Alice 先手,总共有 n 个石子排成一行。
- 轮到某个玩家的回合时,如果石子的数目 大于 1 ,他将执行以下操作:
-
- 选择一个整数 x > 1 ,并且 移除 最左边的 x 个石子;
-
- 将移除的石子价值之和累加到该玩家的分数中;
-
- 将一个新的石子放在最左边,且新石子的值为被移除石子值之和;
- 当只剩下一个石子时,游戏结束。
- Alice 和 Bob 的 分数之差 为 (Alice 的分数 - Bob 的分数) ,Alice 的目标是最大化分数差,Bob 的目标是最小化分数差。
- 给你一个长度为 n 的整数数组 stones ,其中 stones[i] 是 从左边起第 i 个石子的价值,请你返回在双方都采用最优策略的情况下,Alice 和 Bob 的分数之差。
- 示例 1:
输入:stones = [-1,2,-3,4,-5]
输出:5
解释:
- Alice 移除最左边的 4 个石子,得分增加 (-1) + 2 + (-3) + 4 = 2 ,并且将一个价值为 2 的石子放在最左边。stones = [2,-5] 。
- Bob 移除最左边的 2 个石子,得分增加 2 + (-5) = -3 ,并且将一个价值为 -3 的石子放在最左边。stones = [-3] 。
两者分数之差为 2 - (-3) = 5。
- 1
- 2
- 3
- 4
- 5
- 6
- 示例 2:
输入:stones = [7,-6,5,10,5,-2,-6]
输出:13
解释:
- Alice 移除所有石子,得分增加 7 + (-6) + 5 + 10 + 5 + (-2) + (-6) = 13 ,并且将一个价值为 13 的石子放在最左边。stones = [13] 。
两者分数之差为 13 - 0 = 13。
- 1
- 2
- 3
- 4
- 5
- 示例 3:
输入:stones = [-10,-12]
输出:-22
解释:
- Alice 只有一种操作,就是移除所有石子。得分增加 (-10) + (-12) = -22 ,并且将一个价值为 -22 的石子放在最左边。stones = [-22] 。
两者分数之差为 (-22) - 0 = -22。
- 1
- 2
- 3
- 4
- 5
二、求解算法
① 动态规划(Leetcode 官方解法)
- 在任意一名玩家进行操作之后,剩余的所有石子的价值总和不会发生变化。对于每一步操作,玩家可以把「最左侧」的 x 枚石子进行「价值合并」,并获得与「价值」等价的分数。因此,最左侧的石子的价值一定是 stones 的某一个前缀和,即玩家在每一轮获得的分数一定是初始数组 stones 的某一个前缀和。
- 可以将题目中的游戏转化成如下等价的形式:
-
- 求出 stones 的前缀和数组 pre,其中:
-
- Alice 和 Bob 依次在数组 pre 上进行操作,并且 Alice 先手。在一次操作中,当前玩家可以选择一个下标 u,获得 pre[u] 的分数:
-
-
- 如果当前玩家是 Alice 并且是首次操作,那么 u 不能为 0。对应到题目中的游戏规则,即为「选择的石子数量
x 必须大于 1」;
- 如果当前玩家是 Alice 并且是首次操作,那么 u 不能为 0。对应到题目中的游戏规则,即为「选择的石子数量
-
-
-
- 对于其余的情况,如果对手的上一次操作选择的下标是 v,那么必须有 u>v。对应到题目中的游戏规则,即为「对手上一次操作合并了若干枚石子,使得最左侧的石子的价值为 pre[v]」,同时「当前玩家合并了从最左侧的石子开始,到原本在数组 stones 中下标为 u 的石子为止的所有石子,使得最左侧的石子的价值为 pre[u]」。
-
-
- 设数组 stones 和 pre 的长度为 n,如果当前玩家选择了下标 n−1,那么它就会合并剩余的所有石子,游戏结束。
- 可以使用基于博弈思想的动态规划解决上述的游戏,设 f(i) 表示当 Alice 可以选择的下标 u 在 [i,n) 范围内时,Alice 与 Bob 分数的最大差值,在进行状态转移时,可以考虑 Alice 是否选择了 i 作为下标 u;
-
- 如果 Alice 没有选择 i 作为下标 u,那么她需要在 [i+1,n) 的范围内进行选择,因此有状态转移方程:
-
- 如果 Alice 选择了 i 作为下标 u,那么她获得了 pre[i] 的分数,并且轮到 Bob 在剩余的范围 [i+1,n) 中进行选择。由于 Bob 会采用最优策略,因此在 [i+1,n) 的范围内,Bob 与 Alice 分数的最大差值就为 f[i+1],因此有状态转移方程:
- 由于 Alice 会采用最优策略,因此状态转移选择二者中的较大值:
- 从 i=n−1 开始倒序地计算所有的状态,最终的答案即为 f[1]。
- C++ 示例:
class Solution {
public:
int stoneGameVIII(vector<int>& stones) {
int n = stones.size();
vector<int> pre;
partial_sum(stones.begin(), stones.end(), back_inserter(pre));
vector<int> f(n);
f[n - 1] = pre[n - 1];
for (int i = n - 2; i >= 1; --i) {
f[i] = max(f[i + 1], pre[i] - f[i + 1]);
}
return f[1];
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 复杂度分析:
-
- 时间复杂度:O(n)。
-
- 空间复杂度:O(n),即为数组 pre 和 f 需要的空间。
② 暴力法
- 核心思路:通过理解题意,不难发现,当取走左边若干个石子后,对右边石子原来的分数是没有影响的,仍是前缀和,所以预处理一个前缀和是很显然的。
int[] sum = new int[n + 1];
for(int i = 0; i < n; i++) {
sum[i + 1] = sum[i] + stones[i];
}
- 1
- 2
- 3
- 4
- 游戏过程不妨先不考虑时间的要求,直接通过暴力模拟来解决。
- 暴力法直接模拟游戏过程,需要注意每一轮得到的结果都是这一轮的玩家期望得分差值的最大值。如果当前已经取到第 i (1 <= i <= n) 块石子,那么这一轮可以取到的结果 solve(i) 就是从 i 到 n 中选择一个位置 j,使得 sum[j] - (下一轮对手的得分)最大,这里的 sum[j] 就是这一轮的得分,由于要保证双方均采用最优策略,下一轮对手也会选择最大的得分差值,所以相当于求解 sum[j] - solve(j + 1) 的最大值。
- Java 示例:
class Solution {
int n;
int[] stones;
int[] sum;
public int stoneGameVIII(int[] stones) {
n = stones.length;
this.stones = stones;
sum = new int[n + 1];
for(int i = 0; i < n; i++) sum[i + 1] = sum[i] + stones[i];
return solve(2);
}
public int solve(int idx){
if(idx == n) return sum[idx];
int res = sum[n];
for(int i = idx; i < n; i++){
res = Math.max(res, sum[i] - solve(i + 1));
}
return res;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 记忆化递归:完全模拟达到指数级别的时间复杂度,肯定需要进行优化,递归加优化最常见的就是加一个备忘录:
class Solution {
int n;
int[] stones;
int[] sum;
Integer[] memo;
public int stoneGameVIII(int[] stones) {
n = stones.length;
this.stones = stones;
memo = new Integer[n + 1];
sum = new int[n + 1];
for(int i = 0; i < n; i++) sum[i + 1] = sum[i] + stones[i];
memo[n] = sum[n];
return solve(2);
}
public int solve(int idx){
if(memo[idx] != null) return memo[idx];
int res = sum[n];
for(int i = idx; i < n; i++){
res = Math.max(res, sum[i] - solve(i + 1));
}
return memo[idx] = res;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 记忆化过程还是很简单的,直接加个备忘录就可以了,不过这样还是O(N2)的时间复杂度,还是会超时的。
- 在记忆化中,每次递归都要从当前位置向后遍历找到最大的满足条件的值,时间消耗较大,而每个位置都只与他后边的值有关,不妨来看一下 solve(x) 的值到底等于什么:
solve(x) = max(sum[x] - solve(x + 1), sum[x + 1] - solve(x + 2), ... , sum[n - 1] - solve(n), sum[n] - solve(n + 1))
- 1
- 而后边这一段 max(sum[x + 1] - solve(x + 2), … , sum[n - 1] - solve(n), sum[n] - solve(n + 1)),恰好是 solve(x + 1) 的值,带入也就得到:
solve(x) = Math.max(solve(x + 1), sum[x] - solve(x + 1))
- 1
- 这样就可以得到优化到 O(N) 时间复杂度的代码。
class Solution {
int n;
int[] stones;
int[] sum;
Integer[] memo;
public int stoneGameVIII(int[] stones) {
n = stones.length;
this.stones = stones;
memo = new Integer[n + 1];
sum = new int[n + 1];
for(int i = 0; i < n; i++) sum[i + 1] = sum[i] + stones[i];
memo[n] = sum[n];
return solve(2);
}
public int solve(int idx){
if(memo[idx] != null) return memo[idx];
int res = Math.max(solve(idx + 1), sum[idx] - solve(idx + 1));
return memo[idx] = res;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
文章来源: blog.csdn.net,作者:Serendipity·y,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/Forever_wj/article/details/122283518
【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)