石子游戏 II
题目描述
给定一个整数数组 piles,表示一排石子堆,每堆石子数量为 piles[i]。Alice 和 Bob 轮流进行游戏,Alice 先手。在每一轮中,玩家可以取走当前最左边的 X 堆石子,其中 1 <= X <= 2M。然后,将 M 更新为 max(M, X)。游戏持续到所有石子都被取走。假设 Alice 和 Bob 都发挥出最佳水平,目标是让 Alice 能够获得的最大石子总数。
解题过程
-
问题分析
这是一个双人零和博弈问题。与标准的“石子游戏”不同,玩家每次可以取走的堆数X是动态变化的,它取决于一个状态变量M,而M又会在每次操作后更新。这增加了问题的复杂性。我们需要一个动态规划状态来记录当前剩余石子堆的起始位置以及当前的M值。 -
定义状态
设dp[i][m]表示当游戏进行到石子堆从第i堆(0-indexed)开始,并且当前的M值为m时,当前行动的玩家(无论是 Alice 还是 Bob)能够比对手多获得的最大石子总数。
注意:这里的“当前玩家”是相对于状态(i, m)而言的。 -
状态转移方程
在当前状态(i, m)下,当前玩家可以选择取走X堆石子,其中X的取值范围是1到2m。当然,X不能超过剩余的石子堆数,即X <= n - i。
如果玩家取走了X堆石子(即从i到i + X - 1),那么他立即获得这些堆的石子总数,记为stonesTaken = piles[i] + piles[i+1] + ... + piles[i+X-1]。
取走后,游戏状态变为:剩余石子堆从第i + X堆开始,新的M值变为newM = max(m, X)。
此时,轮到对手行动。在状态(i+X, newM)下,对手作为“当前玩家”,会采取最优策略,从而获得相对于“下一个玩家”(即原来的玩家)的最大优势分数,这个分数就是dp[i+X][newM]。
那么,对于当前玩家而言,他这次选择X所带来的净优势是多少呢?- 他通过这次取石子,直接获得了
stonesTaken的分数。 - 但是,接下来对手在状态
(i+X, newM)下行动,会获得相对于他的优势dp[i+X][newM]。这意味着,当前玩家相对于对手的优势会减少dp[i+X][newM]。
因此,选择X后,当前玩家相对于对手的净优势为:stonesTaken - dp[i+X][newM]。
当前玩家的目标就是最大化这个净优势。所以状态转移方程为:
dp[i][m] = max( stonesTaken - dp[i+X][max(m, X)] ),其中X取遍1到min(2m, n-i)。
- 他通过这次取石子,直接获得了
-
边界条件
当没有石子剩余时,即i == n,任何玩家都无法再获得石子,所以dp[n][m] = 0。 -
计算顺序与预处理
- 由于状态
dp[i][m]依赖于dp[i+X][newM],其中i+X > i,所以我们需要从右向左遍历i(即从石子堆的末尾开始计算)。 m的最大值是多少?在最开始,M=1。每次取石子的堆数X最多可以达到2m,而新的M会变成max(m, X)。理论上,m最大可以达到n(当某次取了所有剩余石子时)。所以m的维度可以设为n+1。- 为了快速计算任意连续子数组的和(即
stonesTaken),我们可以预先计算一个前缀和数组prefixSum,使得prefixSum[i]表示piles[0] + piles[1] + ... + piles[i-1]。这样,从i到j的石子数和就是prefixSum[j+1] - prefixSum[i]。在我们的状态转移中,stonesTaken = prefixSum[i+X] - prefixSum[i]。
- 由于状态
-
最终结果
游戏的初始状态是:剩余石子堆从第0堆开始,M=1。当前行动的玩家是 Alice。
我们要求的是 Alice 能获得的最大石子总数。
设总石子数为total = sum(piles)。
根据dp[0][1]的定义:它是 Alice 在初始状态下,相对于 Bob 所能获得的最大优势分数。
假设 Alice 最终获得A分,Bob 获得B分,有A + B = total。
根据优势分数的定义,有A - B = dp[0][1]。
解这个方程组:A = (total + dp[0][1]) / 2。
因为题目保证石子总数为正整数,所以(total + dp[0][1])一定是偶数,结果即为整数。 -
算法步骤总结
a. 计算数组长度n。
b. 计算前缀和数组prefixSum。
c. 初始化一个二维数组dp,维度为(n+1) x (n+1),所有值初始化为一个很小的数(或0,但边界条件会覆盖)。
d. 将dp[n][m](对于所有m)设为 0(边界条件)。
e. 从i = n-1开始,向下遍历到i = 0。
f. 对于每个i,遍历所有可能的m(从 1 到n,但m很大时可能没有对应的X,可以适当剪枝,或者遍历到n也无妨)。
g. 对于每个(i, m),遍历所有可能的X(从 1 到min(2*m, n-i))。
h. 计算本次取走的石子数stonesTaken = prefixSum[i+X] - prefixSum[i]。
i. 计算新的M:newM = max(m, X)。
j. 状态转移:dp[i][m] = max(dp[i][m], stonesTaken - dp[i+X][newM])。
k. 遍历结束后,计算总石子数total = prefixSum[n]。
l. 最终结果(total + dp[0][1]) / 2。
通过这个循序渐进的推导和计算过程,我们就可以解决“石子游戏 II”这个区间动态规划问题。