1. 问题背景与理解
石子游戏II是LeetCode上一道经典的动态规划题目,编号1140。这道题属于博弈类动态规划的典型代表,考察玩家在面对游戏规则时的最优决策能力。题目描述两位玩家(爱丽丝和鲍勃)轮流从石子堆中取石子,每次可以取走前X堆(1 ≤ X ≤ 2M),然后M变为max(M, X)。游戏的目标是获得最多的石子。
这类博弈问题在实际中有诸多应用场景,比如资源分配、投资决策等。理解这类问题的解法不仅能帮助我们提升算法能力,更能培养面对复杂决策时的系统性思维。
2. 问题分析与解法思路
2.1 游戏规则解析
游戏规则有几个关键点需要注意:
- 玩家每次可以取走前1到2M堆石子
- 取走X堆后,M会更新为max(M, X)
- 两位玩家都采取最优策略
- 爱丽丝先手
这些规则决定了我们需要考虑:
- 当前剩余石子堆的状态
- 当前M值
- 当前玩家的选择对后续游戏的影响
2.2 动态规划状态定义
基于上述分析,我们可以定义dp[i][m]表示:
- 当剩下第i堆到最后一堆石子时
- 当前M值为m
- 当前玩家能获得的最大石子数
这个状态定义涵盖了游戏的所有关键信息,为后续的状态转移奠定了基础。
2.3 状态转移方程
状态转移需要考虑当前玩家所有可能的取法(1到2m堆),并选择对自己最有利的方案:
dp[i][m] = max( sum(piles[i:i+x]) + (sum(piles[i+x:]) - dp[i+x][max(m,x)]) for x in range(1, 2*m+1) )
其中:
- sum(piles[i:i+x])是当前玩家这次取走的石子数
- sum(piles[i+x:]) - dp[i+x][max(m,x)]是当前玩家在对手采取最优策略后能获得的剩余石子数
3. 具体实现与优化
3.1 基础实现
首先我们可以实现一个记忆化搜索的版本:
def stoneGameII(piles): n = len(piles) prefix = [0]*(n+1) for i in range(n-1, -1, -1): prefix[i] = prefix[i+1] + piles[i] from functools import lru_cache @lru_cache(maxsize=None) def dp(i, m): if i + 2*m >= n: return prefix[i] max_stones = 0 for x in range(1, 2*m+1): next_m = max(m, x) current = prefix[i] - prefix[i+x] remaining = prefix[i+x] - dp(i+x, next_m) total = current + remaining if total > max_stones: max_stones = total return max_stones return dp(0, 1)3.2 优化技巧
- 前缀和优化:预先计算前缀和数组,可以快速计算任意区间的和
- 记忆化搜索:使用lru_cache避免重复计算
- 边界条件处理:当剩余石子堆数≤2m时,可以直接取走所有石子
3.3 时间复杂度分析
- 状态总数:O(n^2)(i从0到n-1,m从1到n)
- 每个状态的转移:O(n)(x最多取n次)
- 总时间复杂度:O(n^3)
- 空间复杂度:O(n^2)
4. 常见问题与调试技巧
4.1 常见错误
- 状态定义不完整:忘记考虑M的变化
- 边界条件处理不当:特别是当i+x超过数组长度时
- 石子数计算错误:没有使用前缀和导致计算效率低下
4.2 调试建议
- 从小规模测试用例开始验证
- 打印中间状态帮助理解
- 对比暴力解法的结果
4.3 测试用例设计
好的测试用例应该包括:
- 最小规模(1-2堆石子)
- M变化较大的情况
- 石子数分布不均匀的情况
- 边界条件(如所有石子数相同)
例如:
[2,7,9,4,4] → 10 [1,2,3,4,5,100] → 104 [1] → 1 [1,1,1,1] → 25. 算法扩展与变种
5.1 类似题目
- Stone Game I (LeetCode 877)
- Stone Game III (LeetCode 1406)
- Stone Game IV (LeetCode 1510)
5.2 变种思考
- 如果每次可以取1到k*M堆石子(k为常数)
- 如果M的更新规则不同
- 如果石子堆成环形排列
5.3 实际应用
这类博弈动态规划问题可以应用于:
- 资源分配决策
- 投资策略优化
- 游戏AI设计
6. 个人解题心得
在实际解决这个问题时,我发现有几个关键点特别重要:
- 明确状态定义:必须包含所有影响决策的关键信息
- 理解对手的最优策略:这是博弈问题的核心
- 从简单情况入手:先考虑边界条件和小规模情况
- 优化计算效率:前缀和和记忆化是必不可少的工具
这道题教会我们,面对复杂决策问题时,系统地分解问题、定义状态、考虑对手反应,是找到最优解的关键。这种思维方式不仅在算法竞赛中有用,在实际生活和工作中也同样适用。