石子游戏 IX
题目描述
Alice 和 Bob 轮流从一堆石子中取石子,Alice 先手。这堆石子总共有 n 个石子,每个石子有一个价值,价值只能是 1、2 或 3。游戏的规则如下:
- 每个玩家在自己的回合,可以从石子堆的剩余石子中取出一个石子。
- 取出石子后,如果玩家取出的石子价值与之前已被取出的所有石子的价值总和(即历史价值总和)模 3 等于 0,则该玩家输掉游戏。
- 如果所有石子都被取完,仍未有玩家触发输的条件,则最后取石子的玩家输掉游戏。
给定一个石子价值数组 stones,其中 stones[i] 表示第 i 个石子的价值(1、2 或 3)。假设两位玩家都采用最优策略,如果 Alice 获胜返回 true,否则返回 false。
解题过程
这个问题可以通过区间动态规划结合状态压缩来解决。由于石子的价值只有 1、2、3 三种,我们可以统计每种价值的石子数量,并用状态 (c1, c2, c3, mod) 来表示当前剩余石子情况,其中 c1, c2, c3 分别表示剩余价值为 1、2、3 的石子数量,mod 表示当前已取石子价值总和模 3 的结果。
步骤 1:问题分析
- 游戏的关键是避免在取石子后使总价值 mod 3 = 0
- 每个玩家轮流取一个石子,石子价值只能是 1、2、3
- 需要判断在最优策略下先手是否能获胜
步骤 2:状态定义
定义 dp[c1][c2][c3][mod] 表示在当前剩余 c1 个价值1石子、c2 个价值2石子、c3 个价值3石子,且当前总价值 mod 3 的情况下,当前玩家是否能获胜。
- dp 值为 true 表示当前玩家能获胜
- dp 值为 false 表示当前玩家会输
步骤 3:状态转移
对于状态 (c1, c2, c3, mod),当前玩家可以选择取:
-
价值为 1 的石子:如果 c1 > 0
- 新 mod = (mod + 1) % 3
- 如果新 mod = 0,当前玩家立即输掉
- 否则,轮到对手在状态 (c1-1, c2, c3, 新 mod) 下行动
-
价值为 2 的石子:如果 c2 > 0
- 新 mod = (mod + 2) % 3
- 如果新 mod = 0,当前玩家立即输掉
- 否则,轮到对手在状态 (c1, c2-1, c3, 新 mod) 下行动
-
价值为 3 的石子:如果 c3 > 0
- 新 mod = (mod + 3) % 3 = mod
- 如果新 mod = 0,当前玩家立即输掉
- 否则,轮到对手在状态 (c1, c2, c3-1, mod) 下行动
状态转移方程:
dp[c1][c2][c3][mod] = 存在一种选择,使得要么:
- 对手在下一状态会输,即 dp[新状态] = false
- 或者当前选择直接获胜(这种情况不会发生,因为选择后不会立即获胜)
步骤 4:边界条件
当没有石子剩余时(c1 = c2 = c3 = 0):
- 根据规则,最后取石子的玩家输掉游戏
- 所以 dp[0][0][0][mod] = false(当前玩家输)
步骤 5:算法实现
使用记忆化搜索实现:
def stoneGameIX(stones):
from functools import lru_cache
count = [0, 0, 0]
for stone in stones:
count[stone % 3] += 1
@lru_cache(None)
def dfs(c1, c2, c3, mod):
# 如果没有石子可取了
if c1 == 0 and c2 == 0 and c3 == 0:
return False # 最后取石子的玩家输
result = False
# 尝试取价值为1的石子
if c1 > 0:
new_mod = (mod + 1) % 3
if new_mod != 0: # 如果不会立即输
if not dfs(c1-1, c2, c3, new_mod):
result = True
# 尝试取价值为2的石子
if c2 > 0:
new_mod = (mod + 2) % 3
if new_mod != 0: # 如果不会立即输
if not dfs(c1, c2-1, c3, new_mod):
result = True
# 尝试取价值为3的石子
if c3 > 0:
new_mod = mod # (mod + 3) % 3 = mod
if new_mod != 0: # 如果不会立即输
if not dfs(c1, c2, c3-1, new_mod):
result = True
return result
return dfs(count[1], count[2], count[3], 0)
步骤 6:优化分析
实际上,这个问题可以通过数学分析来优化:
- 价值为 3 的石子不会改变 mod 值,可以看作"安全"的石子
- 价值为 1 和 2 的石子会改变 mod 值
- 游戏的关键在于价值 1 和 2 的石子的相对数量
简化规则:
- 如果价值 1 的石子数量为 0:Alice 只能从价值 2 开始,游戏变成模 2 的循环
- 如果价值 2 的石子数量为 0:Alice 只能从价值 1 开始,游戏变成模 1 的循环
- 如果两者都有,Alice 可以选择最优开局
步骤 7:最终简化解法
def stoneGameIX(stones):
cnt = [0, 0, 0]
for stone in stones:
cnt[stone % 3] += 1
# 如果价值1或价值2的石子数量为0
if cnt[1] == 0:
# 只能从价值2开始,需要至少2个价值2的石子
return cnt[2] > 2 and cnt[0] % 2 == 1
if cnt[2] == 0:
# 只能从价值1开始,需要至少2个价值1的石子
return cnt[1] > 2 and cnt[0] % 2 == 1
# 一般情况:Alice可以选择最优开局
return abs(cnt[1] - cnt[2]) > 2 or cnt[0] % 2 == 0
这个问题的关键在于理解模 3 的循环特性,以及如何通过分析石子价值的分布来判断先手优势。区间动态规划提供了理论基础,而数学优化则给出了高效的解决方案。