Skip to content

M877.石子游戏 ​

game theory, dp, https://leetcode.cn/problems/stone-game/

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 正 整数颗石子,数目为 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的 总数 是 奇数 ,所以没有平局。

Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始 或 结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜 。

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false 。

示例 1:

输入:piles = [5,3,4,5]
输出:true
解释:
Alice 先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对 Alice 来说是一个胜利的举动,所以返回 true 。

示例 2:

输入:piles = [3,7,2,3]
输出:true

提示:

  • 2 <= piles.length <= 500
  • piles.length 是 偶数
  • 1 <= piles[i] <= 500
  • sum(piles[i]) 是 奇数

这道题有两种解法:数学思维解法(一行代码) 和 动态规划解法(通解)。


解法一:数学思维 / 博弈论分析(最佳解法) ​

由于题目给出了非常特殊的限制条件:

  1. 石子堆数 n 是 偶数。
  2. 石子总数是 奇数(没有平局)。

我们可以将石子按照索引分为 奇数位 和 偶数位 两组:

  • 奇数位石子:piles[0], piles[2], piles[4], ...
  • 偶数位石子:piles[1], piles[3], piles[5], ...

因为石子总数是奇数,所以 奇数位石子的总和 和 偶数位石子的总和 必然不相等,必定有一个更大。

Alice(先手)控制局面的策略:

  • 如果 Alice 想拿走所有奇数位的石子:第一步拿 piles[0]。此时剩下的两端都是偶数位(piles[1] 和 piles[n-1]),Bob 无论拿哪一边,拿到的都是偶数位。接下来 Alice 又面对两个奇数位,可以继续拿奇数位。
  • 同理,如果 Alice 想拿走所有偶数位的石子:第一步拿 piles[n-1] 即可。

因此,Alice 在游戏开始前就已经知道了是奇数位石子多还是偶数位石子多,并且可以强制拿走多的一方。因为 Alice 总是选择多的那一组,所以 Alice 必胜。

Python3 代码:

python
class Solution:

    def stoneGame(self, piles: List[int]) -> bool:
        # Alice 必胜,直接返回 True
        return True
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)

解法二:动态规划(适用于更通用情况) ​

如果题目去掉“偶数堆”或“总数为奇数”的限制,我们就需要用动态规划来求解。

DP 状态定义: 定义 dp[i][j] 表示在子数组 piles[i...j] 中,当前玩家 比 另一个玩家 最多能多拿的石子数量。

状态转移方程: 当前玩家有两个选择:

  1. 拿走 piles[i],剩下的区间是 [i+1, j]。此时对手能获得的相对最大分数为 dp[i+1][j],所以当前玩家的分数为 piles[i] - dp[i+1][j]。
  2. 拿走 piles[j],剩下的区间是 [i, j-1]。此时对手能获得的相对最大分数为 dp[i][j-1],所以当前玩家的分数为 piles[j] - dp[i][j-1]。

两项取最大值:

dp[i][j]=max(piles[i]−dp[i+1][j], piles[j]−dp[i][j−1])

Python3 代码:

python
from typing import List


class Solution:

    def stoneGame(self, piles: List[int]) -> bool:
        n = len(piles)

        # dp[i][j] 表示在 piles[i...j] 中,当前玩家比对手多拿的最多石子数
        dp = [[0] * n for _ in range(n)]

        # 基础情况:只有一堆石子时,当前玩家直接拿走
        for i in range(n):
            dp[i][i] = piles[i]

        # 按区间长度从小到大填表
        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 1
                dp[i][j] = max(
                    piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]
                )

        # 全局最终结果:若 Alice 能多拿的石子数 > 0,则 Alice 获胜
        return dp[0][n - 1] > 0
  • 时间复杂度:O(n2),需要计算 n2/2 个状态。
  • 空间复杂度:O(n2),即 DP 数组的大小(可以空间优化到 O(n))。

总结

面试中遇到这道题,建议先解释数学博弈逻辑直接输出 True,然后再补充写出动态规划代码展现算法功底。