1901年,哈佛大学教授Charles L. Bouton在《数学年鉴》上发表了一篇论文,题为"Nim, A Game with a Complete Mathematical Theory"。这篇论文揭示了一个令人震惊的事实:一种看似需要随机应变的策略游戏,竟然可以用一个简单的数学公式完全预测胜负。这个公式就是——异或和(XOR sum)。

这个发现的意义远超游戏本身。它为组合博弈论奠定了基础,催生了Grundy数、Sprague-Grundy定理等重要理论,并最终演化为现代人工智能中的极大极小算法和Alpha-Beta剪枝。在LeetCode上,博弈论题目形成了一个独特的类别,从简单的Nim Game到复杂的Stone Game系列,考察的都是同一套核心思维。

公平组合游戏:博弈论的基石

博弈论算法研究的对象是"公平组合游戏"(Impartial Combinatorial Game)。这类游戏具有三个核心特征:

公平性:两名玩家可用的操作完全相同,游戏状态是公开的,不存在隐藏信息。谁先手谁后手是唯一的区别。

有限性:游戏必然在有限步内结束,不存在无限循环的可能。

确定性:游戏结局完全由玩家的决策决定,不存在掷骰子或抽牌等随机因素。

满足这三个条件的游戏,每个状态都可以被精确地划分为"必胜态"(N-position)或"必败态"(P-position):

  • 必胜态:存在至少一种操作可以将对手送入必败态
  • 必败态:无论采取何种操作,都会将对手送入必胜态

这个分类是博弈论所有算法的基础。判断一个状态是必胜还是必败,只需要递归地分析其后继状态。如果存在通往必败态的路径,当前状态就是必胜态;如果所有路径都通向必胜态,当前状态就是必败态。

Nim博弈:异或和的魔法

Nim是最经典的公平组合游戏。游戏规则极其简单:有若干堆石子,两名玩家轮流行动,每次可以从任意一堆中取出任意数量的石子(至少取一颗),取走最后一颗石子的人获胜。

假设当前有三堆石子,数量分别为3、4、5。谁有必胜策略?直觉上,这似乎需要分析大量可能的走法。但Bouton的论文给出了一个惊人的结论:只需计算所有堆大小的异或和,若结果非零则先手必胜,若为零则先手必败

异或和的计算方法

异或(XOR)是一种位运算,规则是"相同为0,不同为1"。计算异或和时,将所有数字转换为二进制,然后逐位进行异或运算:

3 = 011
4 = 100  
5 = 101
--------
XOR = 010 = 2(十进制)

由于异或和为2(非零),先手必胜。

为什么异或和能预测胜负?

证明的核心在于两个关键性质:

性质一:从异或和为零的状态出发,任何操作都会导致异或和非零。

设当前状态为 $(a_1, a_2, ..., a_n)$,异或和 $s = a_1 \oplus a_2 \oplus ... \oplus a_n = 0$。如果从第 $i$ 堆取走石子,使 $a_i$ 变为 $a_i'$,新的异或和为:

$$s' = s \oplus a_i \oplus a_i' = a_i \oplus a_i'$$

由于 $a_i' < a_i$,两者必然不相等,因此 $s' \neq 0$。

性质二:从异或和非零的状态出发,总存在一种操作可以使异或和变为零。

设当前异或和为 $s \neq 0$,找到 $s$ 的最高非零位 $d$。必然存在一堆 $a_i$,其二进制表示在第 $d$ 位为1。将 $a_i$ 变为 $a_i' = a_i \oplus s$,可以证明 $a_i' < a_i$(因为高位变小了),这是一步合法操作。新的异或和为:

$$s' = s \oplus a_i \oplus a_i' = s \oplus a_i \oplus (a_i \oplus s) = 0$$

这两个性质揭示了必胜策略:始终保持异或和为零,对手就无法避免地将异或和变为非零,如此往复,最终你将取走最后一颗石子。

Nim博弈异或和计算示意图

图片来源: Plus Magazine - Play to win with Nim

LeetCode 292. Nim Game

这是Nim博弈最简单的变种:只有一堆石子,每次可以取1到3颗。

class Solution {
    public boolean canWinNim(int n) {
        // 如果石子数是4的倍数,先手必败
        // 否则先手可以取走 n % 4 颗,留给对手4的倍数
        return n % 4 != 0;
    }
}

这道题的关键在于识别模式。当 $n \leq 3$ 时先手直接取完获胜;当 $n = 4$ 时无论取1、2、还是3颗,对手都能取完剩余石子获胜;当 $n = 5, 6, 7$ 时先手可以分别取走1、2、3颗,留给对手4颗。规律清晰:4的倍数是必败态,其他都是必胜态。

时间复杂度:$O(1)$,仅需一次取模运算。

空间复杂度:$O(1)$。

Sprague-Grundy定理:将任意游戏转化为Nim

Nim博弈的异或和策略虽然优雅,但现实中的游戏规则往往更加复杂。1935年和1939年,Roland Sprague和Patrick Michael Grundy独立证明了同一个定理,彻底改变了这一局面:任何公平组合游戏都等价于某个规模的Nim博弈

Grundy数的定义

对于游戏中的任意状态 $v$,定义其Grundy数(也称Nimber)为:

$$G(v) = \text{mex}\{G(u) : u \text{ 是 } v \text{ 的后继状态}\}$$

其中 $\text{mex}$(minimum excludant)函数返回不在集合中的最小非负整数。

Grundy数为0的状态是必败态,Grundy数大于0的状态是必胜态。这为分析任意公平组合游戏提供了统一的框架。

组合游戏的Grundy数

当游戏由多个独立的子游戏组成时(比如Nim中的多堆石子),总状态的Grundy数等于各子游戏Grundy数的异或和:

$$G(\text{total}) = G_1 \oplus G_2 \oplus ... \oplus G_n$$

这就是Sprague-Grundy定理的核心结论。它告诉我们:分析复杂游戏,只需计算每个子游戏的Grundy数,然后异或起来。

计算Grundy数的示例

考虑一个游戏:有一堆 $n$ 颗石子,每次可以取走1、2或3颗。计算各状态的Grundy数:

  • $G(0) = \text{mex}\{\} = 0$(没有后继状态)
  • $G(1) = \text{mex}\{G(0)\} = \text{mex}\{0\} = 1$
  • $G(2) = \text{mex}\{G(1), G(0)\} = \text{mex}\{1, 0\} = 2$
  • $G(3) = \text{mex}\{G(2), G(1), G(0)\} = \text{mex}\{2, 1, 0\} = 3$
  • $G(4) = \text{mex}\{G(3), G(2), G(1)\} = \text{mex}\{3, 2, 1\} = 0$
  • $G(5) = \text{mex}\{G(4), G(3), G(2)\} = \text{mex}\{0, 3, 2\} = 1$

规律显现:$G(n) = n \mod 4$。这与LeetCode 292的结论一致。

极大极小算法:零和博弈的最优策略

并非所有博弈论问题都能用Nim理论解决。当游戏不再"公平"(双方可操作不同)时,需要更通用的框架——极大极小算法(Minimax)。

零和博弈的基本假设

极大极小算法适用于"零和博弈":一方收益等于另一方损失。在此假设下,算法模拟双方都采取最优策略的游戏过程:

  • 最大化玩家(MAX):选择使评估函数最大的行动
  • 最小化玩家(MIN):选择使评估函数最小的行动

算法框架

public int minimax(int depth, boolean isMaximizingPlayer) {
    // 终止条件:达到最大深度或游戏结束
    if (depth == 0 || isGameOver()) {
        return evaluate();
    }
    
    if (isMaximizingPlayer) {
        int maxEval = Integer.MIN_VALUE;
        for (Move move : getAllPossibleMoves()) {
            makeMove(move);
            int eval = minimax(depth - 1, false);
            undoMove(move);
            maxEval = Math.max(maxEval, eval);
        }
        return maxEval;
    } else {
        int minEval = Integer.MAX_VALUE;
        for (Move move : getAllPossibleMoves()) {
            makeMove(move);
            int eval = minimax(depth - 1, true);
            undoMove(move);
            minEval = Math.min(minEval, eval);
        }
        return minEval;
    }
}

算法的时间复杂度为 $O(b^d)$,其中 $b$ 是分支因子(每步平均可行走法数),$d$ 是搜索深度。对于井字棋这样的简单游戏,$b \approx 5, d \approx 9$,完全可以穷举。但对于国际象棋($b \approx 35, d \approx 100$),直接使用极大极小算法不可行。

LeetCode 486. Predict the Winner

这道题是极大极小算法的经典应用:给定一个数组,两名玩家轮流从两端取数,预测先手是否能获胜。

class Solution {
    public boolean PredictTheWinner(int[] nums) {
        int n = nums.length;
        // dp[i][j] 表示在区间 [i, j] 内先手能获得的最大分数差
        int[][] dp = new int[n][n];
        
        // 基本情况:只有一个数时,先手直接取走
        for (int i = 0; i < n; i++) {
            dp[i][i] = nums[i];
        }
        
        // 区间DP:从小区间扩展到大区间
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                // 先手取左端:获得 nums[i],然后对手在 [i+1, j] 上先手
                // 先手取右端:获得 nums[j],然后对手在 [i, j-1] 上先手
                // 对手会最大化自己的收益,相当于最小化当前玩家的收益
                dp[i][j] = Math.max(
                    nums[i] - dp[i + 1][j],
                    nums[j] - dp[i][j - 1]
                );
            }
        }
        
        // 先手能获得非负的分数差则获胜
        return dp[0][n - 1] >= 0;
    }
}

这里的关键洞察是:分数差可以简化极大极小的交替逻辑。当前玩家的收益减去对手的收益,本质上就是极大极小算法要优化的目标。

时间复杂度:$O(n^2)$,需要填充 $n^2$ 个状态。

空间复杂度:$O(n^2)$,可优化到 $O(n)$。

Alpha-Beta剪枝:极大极小的加速器

极大极小算法的问题在于搜索空间呈指数增长。Alpha-Beta剪枝是一种优化技术,可以在不影响结果的情况下跳过大量无需搜索的分支。

核心思想

维护两个值:

  • Alpha:MAX玩家当前能保证的最小收益(下界)
  • Beta:MIN玩家当前能保证的最大损失(上界)

当发现某个分支不可能比已知最优解更好时,立即停止搜索该分支。

public int alphabeta(int depth, int alpha, int beta, boolean isMaximizingPlayer) {
    if (depth == 0 || isGameOver()) {
        return evaluate();
    }
    
    if (isMaximizingPlayer) {
        int maxEval = Integer.MIN_VALUE;
        for (Move move : getAllPossibleMoves()) {
            makeMove(move);
            int eval = alphabeta(depth - 1, alpha, beta, false);
            undoMove(move);
            maxEval = Math.max(maxEval, eval);
            alpha = Math.max(alpha, eval);
            if (beta <= alpha) {
                break;  // 剪枝:MIN玩家不会选择这条路径
            }
        }
        return maxEval;
    } else {
        int minEval = Integer.MAX_VALUE;
        for (Move move : getAllPossibleMoves()) {
            makeMove(move);
            int eval = alphabeta(depth - 1, alpha, beta, true);
            undoMove(move);
            minEval = Math.min(minEval, eval);
            beta = Math.min(beta, eval);
            if (beta <= alpha) {
                break;  // 剪枝:MAX玩家不会选择这条路径
            }
        }
        return minEval;
    }
}

Alpha-Beta剪枝示意图

图片来源: HackerEarth - Minimax Algorithm with Alpha-Beta Pruning

剪枝原理图解

假设MAX玩家在某个节点已经找到了一个收益为3的走法。当探索另一个分支时,如果MIN玩家能强制收益降到2以下,MAX玩家就完全没有理由选择这个分支。Alpha-Beta剪枝正是利用这一原理,在搜索过程中动态剪掉不可能更优的分支。

在最优情况下,Alpha-Beta剪枝可以将时间复杂度从 $O(b^d)$ 降低到 $O(b^{d/2})$,相当于搜索深度翻倍。

Stone Game系列:博弈论与动态规划的完美结合

LeetCode上的Stone Game系列是博弈论算法的综合演练场,从简单的数学推导到复杂的状态空间搜索,涵盖了博弈论的各个层面。

LeetCode 877. Stone Game

Alice和Bob轮流从数组两端取石子,Alice先手。由于数组长度为偶数且石子总数为奇数,Alice总是必胜

class Solution {
    public boolean stoneGame(int[] piles) {
        // 数学结论:先手总是必胜
        // 因为Alice可以预先选择所有偶数位置或所有奇数位置
        // 两种方案的总和必有一个大于对方
        return true;
    }
}

如果需要计算具体分数,则使用动态规划:

class Solution {
    public boolean stoneGame(int[] piles) {
        int n = piles.length;
        int[][] dp = new int[n][n];
        
        for (int i = 0; i < n; i++) {
            dp[i][i] = piles[i];
        }
        
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                dp[i][j] = Math.max(
                    piles[i] - dp[i + 1][j],
                    piles[j] - dp[i][j - 1]
                );
            }
        }
        
        return dp[0][n - 1] > 0;
    }
}

LeetCode 1140. Stone Game II

规则升级:每轮可以取前 $X$ 堆石子($1 \leq X \leq 2M$),然后 $M$ 更新为 $\max(M, X)$。

class Solution {
    private int[] prefixSum;
    private Integer[][] memo;
    private int n;
    
    public int stoneGameII(int[] piles) {
        n = piles.length;
        prefixSum = new int[n + 1];
        memo = new Integer[n][n + 1];
        
        for (int i = 0; i < n; i++) {
            prefixSum[i + 1] = prefixSum[i] + piles[i];
        }
        
        return dfs(0, 1);
    }
    
    private int dfs(int i, int m) {
        // 如果能取走所有剩余石子,直接取走
        if (m * 2 >= n - i) {
            return prefixSum[n] - prefixSum[i];
        }
        
        if (memo[i][m] != null) {
            return memo[i][m];
        }
        
        int result = 0;
        for (int x = 1; x <= 2 * m; x++) {
            // 当前玩家取走前x堆,获得这些石子
            // 对手在剩余石子上的最优收益通过递归计算
            // 当前玩家的总收益 = 取走的石子 + 剩余石子 - 对手的收益
            int currentGain = prefixSum[i + x] - prefixSum[i];
            int remainingStones = prefixSum[n] - prefixSum[i + x];
            int opponentGain = dfs(i + x, Math.max(m, x));
            result = Math.max(result, currentGain + remainingStones - opponentGain);
        }
        
        memo[i][m] = result;
        return result;
    }
}

时间复杂度:$O(n^3)$,状态数为 $O(n^2)$,每个状态需要遍历 $O(n)$ 个选择。

空间复杂度:$O(n^2)$。

LeetCode 1510. Stone Game IV

给定 $n$ 颗石子,玩家轮流取走完全平方数颗石子。取走最后一颗者获胜。

class Solution {
    public boolean winnerSquareGame(int n) {
        // dp[i] = true 表示剩余 i 颗石子时当前玩家必胜
        boolean[] dp = new boolean[n + 1];
        
        for (int i = 1; i <= n; i++) {
            // 尝试所有可能的完全平方数
            for (int k = 1; k * k <= i; k++) {
                // 如果存在一种取法使得对手必败,则当前玩家必胜
                if (!dp[i - k * k]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        
        return dp[n];
    }
}

这道题的本质是计算每个状态的Grundy数,只是状态转移更简单——Grundy数大于0就是必胜态。

时间复杂度:$O(n\sqrt{n})$。

空间复杂度:$O(n)$。

博弈论算法的统一框架

回顾上述所有问题,可以总结出一个统一的解题框架:

类型一:数学推导型

当游戏规则简单且有明显的数学规律时,直接推导公式:

  • Nim博弈:异或和判断
  • Stone Game I:偶数长度保证先手必胜

类型二:Grundy数型

当游戏可以分解为独立子游戏时:

  1. 计算每个子游戏的Grundy数
  2. 异或求和判断必胜/必败态
  3. 必胜时构造必胜策略

类型三:极大极小型

当游戏双方操作不对称或状态空间复杂时:

  1. 定义状态和评估函数
  2. 使用DP或记忆化搜索计算每个状态的值
  3. 当前玩家选择最大化自身收益的操作

LeetCode博弈论题目汇总

题号 题目名称 核心算法 难度
292 Nim Game 数学推导 Easy
877 Stone Game 数学/DP Medium
486 Predict the Winner 极大极小DP Medium
1025 Divisor Game 数学推导 Easy
1140 Stone Game II 记忆化搜索 Medium
1406 Stone Game III DP Hard
1510 Stone Game IV Grundy数DP Hard
1908 Game of Nim Nim理论 Medium
913 Cat and Mouse 极大极小/状态压缩 Hard

从理论到实践:面试中的博弈论

博弈论题目在面试中频繁出现,但大多数都可以用以下模式解决:

第一步:判断游戏类型

是否为公平组合游戏?双方操作是否对称?游戏是否有限?

第二步:寻找必胜/必败态规律

从小规模数据开始,尝试发现必胜态和必败态的分布规律。

第三步:选择合适的算法

  • 规律明显 → 数学公式
  • 可分解 → Grundy数
  • 不对称/复杂 → 极大极小 + DP

第四步:验证边界情况

确保正确处理游戏结束状态、单元素状态等边界条件。

博弈论的魅力在于:看似复杂的多方对抗,往往可以用简洁的数学公式完全刻画。从Nim博弈的异或和,到极大极小算法的递归搜索,再到Alpha-Beta剪枝的优化,这套思维框架不仅适用于编程面试,更是理解人工智能、经济学、政治学等领域的基石。


参考资料

  1. Bouton, C. L. (1901). “Nim, A Game with a Complete Mathematical Theory”. Annals of Mathematics.
  2. CP-Algorithms. “Sprague-Grundy theorem. Nim”. https://cp-algorithms.com/game_theory/sprague-grundy-nim.html
  3. GeeksforGeeks. “Minimax Algorithm in Game Theory”. https://www.geeksforgeeks.org/minimax-algorithm-in-game-theory/
  4. Plus Magazine. “Play to win with Nim”. https://plus.maths.org/play-win-nim
  5. LeetCode. “Game Theory Problem List”. https://leetcode.com/problem-list/game-theory/
  6. USACO Guide. “Game Theory”. https://usaco.guide/adv/game-theory
  7. HackerEarth. “Minimax Algorithm with Alpha-Beta Pruning”. https://www.hackerearth.com/blog/minimax-algorithm-alpha-beta-pruning