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 <= 500piles.length是 偶数1 <= piles[i] <= 500sum(piles[i])是 奇数
这道题有两种解法:数学思维解法(一行代码) 和 动态规划解法(通解)。
解法一:数学思维 / 博弈论分析(最佳解法)
由于题目给出了非常特殊的限制条件:
- 石子堆数
n是 偶数。 - 石子总数是 奇数(没有平局)。
我们可以将石子按照索引分为 奇数位 和 偶数位 两组:
- 奇数位石子:
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 代码:
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
# Alice 必胜,直接返回 True
return True- 时间复杂度:
- 空间复杂度:
解法二:动态规划(适用于更通用情况)
如果题目去掉“偶数堆”或“总数为奇数”的限制,我们就需要用动态规划来求解。
DP 状态定义: 定义 dp[i][j] 表示在子数组 piles[i...j] 中,当前玩家 比 另一个玩家 最多能多拿的石子数量。
状态转移方程: 当前玩家有两个选择:
- 拿走
piles[i],剩下的区间是[i+1, j]。此时对手能获得的相对最大分数为dp[i+1][j],所以当前玩家的分数为piles[i] - dp[i+1][j]。 - 拿走
piles[j],剩下的区间是[i, j-1]。此时对手能获得的相对最大分数为dp[i][j-1],所以当前玩家的分数为piles[j] - dp[i][j-1]。
两项取最大值:
Python3 代码:
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- 时间复杂度:
,需要计算 个状态。 - 空间复杂度:
,即 DP 数组的大小(可以空间优化到 )。
总结
面试中遇到这道题,建议先解释数学博弈逻辑直接输出 True,然后再补充写出动态规划代码展现算法功底。