LeetCode探索(37):1140-石子游戏II

268 阅读1分钟

「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战」。

题目

爱丽丝和鲍勃继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 piles[i]。游戏以谁手中的石子最多来决出胜负。

爱丽丝和鲍勃轮流进行,爱丽丝先开始。最初,M = 1

在每个玩家的回合中,该玩家可以拿走剩下的 X 堆的所有石子,其中 1 <= X <= 2M。然后,令 M = max(M, X)

游戏一直持续到所有石子都被拿走。

假设爱丽丝和鲍勃都发挥出最佳水平,返回爱丽丝可以得到的最大数量的石头。

示例 1:

输入:piles = [2,7,9,4,4]
输出:10
解释:如果一开始Alice取了一堆,Bob取了两堆,然后Alice再取两堆。爱丽丝可以得到2 + 4 + 4 = 10堆。如果Alice一开始拿走了两堆,那么Bob可以拿走剩下的三堆。在这种情况下,Alice得到2 + 7 = 9堆。返回10,因为它更大。

示例 2:

输入:piles = [1,2,3,4,5,100]
输出:104

提示:

  • 1 <= piles.length <= 100
  • 1 <= piles[i] <= 10^4

思考

这是一道博弈论类型的题目,难度中等。乍一看题目有点复杂,相比于石子游戏 I ,本题中玩家可以拿走剩下的 前 X 堆的所有石子,而数字 X 是可选的。

我们可以借助动态规划的方法去解决该问题。

为了方便统计玩家获取的石子数量之和,我们可以借助后缀和,将区间 [i..len - 1] 的后缀和定义为 suffixSum[i],那么区间 [i..j] 的后缀和为suffixSum[i] - suffixSum[j - 1]。考虑到每次取出的石子数量是可变的,那么我们需要将每回合可以拿走的石子数量考虑进来。

我们定义二维数组 dp[i][j],表示从区间 piles[i..len - 1] 里取出 j 堆石子,当前先手能够获得的分数。接着我们就可以列出状态转移方程了。边界条件是,当i + 2 * M >= len时,玩家可以取走[i, len - 1]中的所有堆。

总而言之,本题相比于石子游戏 I ,还是复杂了很多。

解答

方法一:动态规划

/**
 * @param {number[]} piles
 * @return {number}
 */
var stoneGameII = function(piles) {
  const len = piles.length
  // 后缀和
  const suffixSum = new Array(len + 1).fill(0)
  for (let i = len - 1; i >= 0; i--) {
    suffixSum[i] = suffixSum[i + 1] + piles[i]
  }
  // dp[i][j]
  const dp = new Array(len).fill(0).map(() => new Array(len + 1).fill(0))
  for (let i = len - 1; i >= 0; i--) {
    // 枚举 M:从取左边 1 堆石头,到 len 堆石头
    for (let M = 1; M <= len; M++) {
      // [i, len - 1] 这个区间里所有的石头都可以拿走,因为玩家一次最多可以取走 2M 堆石子
      if (i + 2 * M >= len) {
        dp[i][M] = suffixSum[i]
        continue
      }
      // 枚举 X
      for (let X = 1; X <= 2 * M; X++) {
        dp[i][M] = Math.max(dp[i][M], suffixSum[i] - dp[i + X][Math.max(X, M)])
      }
    }
  }
  return dp[0][1]
}
// console.log(stoneGameII([2,7,9,4,4])) // 10
// console.log(stoneGameII([1,2,3,4,5,100])) // 104

参考