小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
例题
题目描述
亚历克斯和李继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子
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 <= piles.length <= 100
1 <= piles[i] <= 10 ^ 4
解题思路
这是一道博弈题,也是一道动态规划题目。动态规划一般需要两个步骤:
- 状态(子问题)定义
- 状态转移方程
比较难的一般是转移方程,写出来还要考虑正向/反向转移顺序,边界等情况。但如果使用记忆化搜索的方式,就完全不需要考虑这些。
回到这道题,我们先定义状态:dp[i][j]
表示已经拿了 i
堆棋子,且此时 M=j
的情况下,剩下这些石子,先拿的人能取得的最大值。那么我们的初始问题就是求 dp[0][1]
。
如何计算 dp[i][j]
?
首先,如果 2j >= 剩下的石子堆数
,则可以拿走全部的石子。
否则,我们可以拿的石子堆数为 1 ~ 2j
,枚举此时玩家 A 拿走的石子堆数 X
,其中 1<=X<=2j
,则轮到 B 拿的时候,B 最多可以拿走 dp[i+X][max(j, X)]
个石子。如果我们知道在 A 拿石子前,这些石子的总个数先设为 total
,则减去 B 拿走的石子数,就是在 A 选择拿走 X
堆石子的情况下可以获取的石子数,即 total - dp[i+X][max(j, X)]
。
至于 total
怎么求,也很简单,只需要先预处理前缀和 prefix[1...n]
,我们知道全部石子数 prefix[n]
,又知道了前 i
堆石子的前缀和 prefix[i]
,则拿走了 i
堆石子后,剩下石子数自然就是 prefix[n] - prefix[i]
。
AC 代码(加了详细注释)
/**
* @param {number[]} piles
* @return {number}
*/
var stoneGameII = function(piles) {
let n = piles.length; // 一共 n 堆石子
// 预处理前缀和
let prefix = new Array(n + 1).fill(0); // prefix[i]表示前i个数的前缀和
for (let i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + piles[i];
}
// 存储结果 初始化 dp[n+1][n+1] 所有值为 -1
let dp = new Array(n + 1).fill().map(() => new Array(n + 1).fill(-1));
// 拿了i个棋子 m=j 的情况下 先拿的人能取得的最大值
function dfs(i, j) {
// 用 dp 存储计算结果 如果已经计算过一次则不需要再次计算
if (dp[i][j] !== -1) {
return dp[i][j];
}
// 当前的全部石子数
let total = prefix[n] - prefix[i];
// 剩下的堆数可以全部拿走的话 则一定会直接全部拿走
if (n - i <= 2 * j) {
return dp[i][j] = total;
}
// 否则枚举拿走的石子堆数
let answer = 0;
for (let x = 1; x <= 2 * j; x++) {
// 先计算在此前提下 另外一个人可以拿到的最大石子数
let other_max = dfs(i + x, Math.max(x, j));
// 则当前用户可以拿到此时的全部石子 - 另外一个人拿到的石子数
answer = Math.max(answer, total - other_max);
}
// 返回结果并把结果存储到 dp
return dp[i][j] = answer;
}
return dfs(0, 1);
};
复制代码
可以看到,用记忆化搜索,不需要考虑计算子问题的先后顺序,只需要用 dfs()
去获取所需要的状态即可,重点在于需要在每次计算结果后都存储起来,因为一个子问题可能被计算多次,不存储的话会超时。
如果看懂了,可以看下这道题 1510. 石子游戏 IV,和本题有类似的思路,不过简单些,一维dp可解。
提示:dp[i]
表示剩余 i
个石子时是否先手赢,然后每次枚举当前玩家拿走的石子数即可。