数据结构与算法之深入解析“石子游戏II”的求解思路与算法示例
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“石子游戏II”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。
一、题目描述
- 亚历克斯和李继续他们的石子游戏,许多堆石子 排成一行,每堆都有正整数颗石子 piles[i],游戏以谁手中的石子最多来决出胜负。亚历克斯和李轮流进行,亚历克斯先开始,最初,M = 1。
- 在每个玩家的回合中,该玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M;然后,令 M = max(M, X),游戏一直持续到所有石子都被拿走。
- 假设亚历克斯和李都发挥出最佳水平,返回亚历克斯可以得到的最大数量的石头。
- 示例:
输入:piles = [2,7,9,4,4]
输出:10
解释:
如果亚历克斯在开始时拿走一堆石子,李拿走两堆,接着亚历克斯也拿走两堆,在这种情况下,亚历克斯可以拿到 2 + 4 + 4 = 10 颗石子。
如果亚历克斯在开始时拿走两堆石子,那么李就可以拿走剩下全部三堆石子,在这种情况下,亚历克斯可以拿到 2 + 7 = 9 颗石子,所以我们返回更大的 10
二、求解思路
① 记忆化搜索
- 总数 = 先手最大 + 后手最大;
- 总数 = 先手选择数量 + 剩余先手最大 + 剩余后手最大;
- 由1,2 => 先手最大 + 后手最大 == 先手选择数量 + 剩余先手最大 + 剩余后手最大;
- 后手最大 == 剩余先手最大;
- 由3,4 => 先手最大 == 先手选择数量 + 剩余后手最大;
- 剩余后手最大 == 剩余数量 - 剩余先手最大;
- 由5,6 => 先手最大 == 先手选择数量 + 剩余数量 - 剩余先手最大;
- 先手选择数量 == pre[takeTo] - pre[index];
- 剩余数量 == pre[pre.length - 1] - pre[takeTo];
- 剩余先手最大 == stoneGameII(pre, takeTo, newm, dp);
- 由7,8,9,10 => 先手最大 == pre[takeTo] - pre[index] + pre[pre.length - 1] - pre[takeTo] - stoneGameII(pre, takeTo, newm, dp);
- 因此 先手最大 == - pre[index] + pre[pre.length - 1] - stoneGameII(pre, takeTo, newm, dp);
- 结果就是先手最大中最大那个 max = Math.max(max, pre[pre.length - 1] - pre[index] - takeNum1);
- int[] pre 为了快速的算出两段中间的累加和。
- Java 示例:
lass Solution
public int stoneGameII(int[] piles)
int len = piles.length;
int[] pre = new int[len + 1];//为了快速的算出两段中间的累加和
for (int i = 0; i < len; i++)
pre[i + 1] = piles[i] + pre[i];
int m = 1;
Integer[][] dp = new Integer[len][len+1];
return stoneGameII(pre, 0, m, dp);// 在剩余len堆中拿到最大
private int stoneGameII(int[] pre, int index, int m, Integer[][] dp) //从index开始(index不可拿),当前先手能拿到的最大值是多少
if (dp[index][m] != null)
return dp[index][m];
int rest = pre.length - 1 - index;
if (2 * m >= rest)
return pre[pre.length - 1] - pre[index];
int max = Integer.MIN_VALUE;
int range = m * 2;
for (int x = 1; x <= range; x++) //注意相互依赖 ,卡了个bug ,这块用m和newm如果用一个变量,错的很离谱
if (x <= rest)
int takeTo = x + index;
// int takeNum = pre[takeTo] - pre[index]; // 没用上
int newm = Math.max(x, m);/// newm 和 range 不能同时用m
int takeNum1 = stoneGameII(pre, takeTo, newm, dp);/// ?
// pre[pre.length - 1] - pre[takeTo] - takeNum1 + pre[takeTo] - pre[index];
max = Math.max(max, pre[pre.length - 1] - pre[index] - takeNum1);
dp[index][m] = max;
return max;
② 动态规划
- 本题很明显要用记忆化搜索或者动态规划来求解,如果直接使用动态规划的话,我们要想清楚有哪些子状态需要存储。
- 首先一定要存储的是取到某一个位置时,已经得到的最大值或者后面能得到的最大值,但是光有位置是不够的,相同的位置有不同数量的堆可以取,所以我们还需存储当前的 M 值。
- 由于本题中的状态是从后往前递推的,如:假如最后只剩一堆,一定能算出来最佳方案,但是剩很多堆时不好算(依赖后面的状态),所以选择从后往前递推。
- 有了思路之后,就能很方便地定义 dp 数组:
-
- dp[i][j] 表示剩余 [i : len - 1] 堆时,M = j 的情况下,先取的人能获得的最多石子数;
-
- i + 2M >= len, dp[i][M] = sum[i : len - 1],剩下的堆数能够直接全部取走,那么最优的情况就是剩下的石子总和;
-
- i + 2M < len, dp[i][M] = max(dp[i][M], sum[i : len - 1] - dp[i + x][max(M, x)]), 其中 1 <= x <= 2M,剩下的堆数不能全部取走,那么最优情况就是让下一个人取的更少。对于所有的 x 取值,下一个人从 x 开始取起,M 变为 max(M, x),所以下一个人能取 dp[i + x][max(M, x)],最多能取 sum[i : len - 1] - dp[i + x][max(M, x)]。
- 对于 piles = [2,7,9,4,4],可以得到下图所示的 dp 数组,结果为 dp[0][1]:
- Java 示例:
import java.util.Arrays;
public class Solution
// 动态规划(后缀数组)
public int stoneGameII(int[] piles)
int len = piles.length;
// 后缀和
int[] suffixSum = new int[len + 1];
suffixSum[len] = 0;
// 区间 [i..len - 1] 的后缀和 = suffixSum[i]
// 区间 [i..j] 的后缀和 suffixSum[i] - suffixSum[j - 1]
for (int i = len - 1; i >= 0; i--)
suffixSum[i] = suffixSum[i + 1] + piles[i];
// dp[i][j] 表示:区间 piles[i..len - 1] 里取出 j 堆石子,当前先手能够获得的分数(注意:不是相对分数)
int[][] dp = new int[len][len + 1];
for (int i = len - 1; i >= 0; i--)
// 枚举 M:从取左边 1 堆石头,到 len 堆石头
for (int M = 1; M <= len; M++)
// 至少要取 1 堆,i + 2 * M >= len 说明 [i, len - 1] 这个区间里所有的石头都可以拿走
if (i + 2 * M >= len)
dp[i][M] = suffixSum[i];
continue;
// 枚举 X,此时 i + 2 * M < len,X <= 2 * M 保证了 i + X < len
for (int X = 1; X <= 2 * M; X++)
// 这里 X 可以理解为下标偏移,也可以理解为选取的石子堆数量
dp[i][M] = Math.max(dp[i][M], suffixSum[i] - dp[i + X][Math.max(X, M)]);
return dp[0][1];
③ 记忆化递归
- 对博弈类问题比较生疏的时候,可以画图去理解「博弈」的过程,在下面这张图上没有办法很详细地展示整个思维过程,因此简单说一下思路:
- 思路分析:
-
- 这样设计状态的(很重要,这样的状态设计会给转移带来一定方便):当前做决策的人,拿石子的时候得分为正,留给下一轮队手选择的时候,得分为负,因此定义的分数是 相对分数;
-
- 这一轮可以选择的石子堆数,与上一轮相关,因此需要设置一个参数 M(和题目中的 M 意思一样),表示当前可以选择的石子堆数;
-
- 从叶子结点开始向上每一步进行选择和比较,这样的做法才叫做「假设亚历克斯和李都发挥出最佳水平」,即每个人都让自己的利益最大化,等价于「让他人利益最小化」,这是因为石子的总数是固定的。
- 由于拿的都是左边的连续的石子堆,需要先计算一下前缀和,然后计算区间和;
- 当前区间(或者说由左边界 begin 决定的剩下的区间,右边界一定是 len - 1)里的石子堆数小于等于 2M 的时候,全部拿走,是使得自己获利最多的做法;
- 否则的话,就需要枚举拿 1 堆、2 堆、3 堆的时候,选出一个当对手利益最大化的时候,自己利益最大化的选择;
- 由于 memo 或者说 dfs 定义的是「相对分数」,因此输出的时候,还要做一个小的转化。
- Java 示例:
public class Solution
// 一开始把这道题和区间 dp 联系在一起,是不对的
public int stoneGameII(int[] piles)
int len = piles.length;
int[][] memo = new int[len][len + 1];
// [i, j] 的前缀和 preSum[j + 1] - preSum[i]
int[] preSum = new int[len + 1];
for (int i = 0; i < len; i++)
preSum[i + 1] = preSum[i] + piles[i];
// x + y = preSum[len];
// x - y = res;
int res = dfs(piles, 0, 1, preSum, memo);
// 由于得到的是相对分数,需要转换成为绝对分数
return (preSum[len] + res) / 2;
/**
* @param piles
* @param begin 定义石子堆的起始下标,即在 [start, len - 1] 这个区间里取石子
* @param M 当前先手可以拿 [1, 2 * M] 堆石子(如果石子数够的话)
* @param memo
* @return 当前玩家在区间 [start, len - 1] 这个区间里取石子,得到的「相对分数」
*/
private int dfs(int[] piles, int begin, int M, int[] preSum, int[][] memo)
int len = piles.length;
if (begin >= len)
return 0;
if (memo[begin][M] != 0)
return memo[begin][M];
// 当前区间 [begin, len - 1] 的元素个数 len - begin <= 2M 的时候,
// 全部拿走是利益最大的,这是因为 1 <= piles[i] <= 10 ^ 4
if (len - begin <= 2 * M)
memo[begin][M] = preSum[len] - preSum[begin];
return preSum[len] - preSum[begin];
// 走到这里,可以取的石子堆数 1 <= X <= 2M
// 区间 [begin, j] 的长度 j - begin + 1 >= 2 * M
int minLen = Math.min(2 * M, len - begin);
// 这个初始化很重要,因为有可能是负分,所以不能初始化为 0
int res = Integer.MIN_VALUE;
for (int X = 1; X <= minLen; X++)
// 区间 [begin, begin + X - 1] 的前缀和 = preSum[begin + X] - preSum[begin - 1]
int chooseLeft = preSum[begin + X] - preSum[begin];
res = Math.max(res, chooseLeft - dfs(piles, begin + X, Math.max(M, X), preSum, memo));
memo[begin][M] = res;
return res;
public static void main(String[] args)
Solution solution = new Solution();
int[] piles = new int[]2, 7, 9, 4, 4;
int res = solution.stoneGameII(piles);
System.out.println(res);
以上是关于数据结构与算法之深入解析“石子游戏II”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章
数据结构与算法之深入解析“股票的最大利润”的求解思路与算法示例