引言
Nim游戏是组合博弈论中最经典的问题之一。游戏规则简单:有若干堆石子,两位玩家轮流从任意一堆中取走至少一颗石子,取走最后一颗石子者获胜。看似简单的规则,却引出了Sprague-Grundy定理这一博弈论的基石。本文将用Java完整实现Nim游戏及其变种,深入讲解SG函数的计算方法与应用。
游戏规则与数学建模
标准Nim规则
- 游戏开始时设有n堆石子,第i堆有 $a_i$ 颗石子
- 两名玩家轮流操作
- 每次操作:选择一堆非空石子,从中取走任意正数颗(至少1颗,最多整堆)
- 取走最后一颗石子的玩家获胜(正常游戏规则)
必胜态与必败态
在组合博弈论中,状态可分为:
– N-position(Next player wins):先手必胜态
– P-position(Previous player wins):先手必败态
Bouton定理指出:Nim游戏的某个局面是P-position(先手必败),当且仅当所有堆的石子数异或和为0。即:
$$a_1 \oplus a_2 \oplus \cdots \oplus a_n = 0$$
基础Nim游戏的Java实现
import java.util.*;
/**
* Nim游戏核心算法实现
* 重点:SG函数、异或和判定、必胜策略生成
*/
public class NimGame {
private int[] piles;
private Random random;
public NimGame(int[] initialPiles) {
this.piles = Arrays.copyOf(initialPiles, initialPiles.length);
this.random = new Random();
}
/**
* 计算当前局面的异或和(nim-sum)
* 若结果为0则为必败态,非0则为必胜态
*/
public int calculateNimSum() {
int xor = 0;
for (int pile : piles) {
xor ^= pile;
}
return xor;
}
/**
* 判断当前是否为必胜态
*/
public boolean isWinningPosition() {
return calculateNimSum() != 0;
}
/**
* 寻找必胜策略:返回一个合法的获胜移动
* 策略:找到最高位的1所在堆,将该堆减少到 pile XOR nimSum
* @return int[]{堆索引, 取走后的剩余数量},若无可行移动返回null
*/
public int[] findWinningMove() {
int nimSum = calculateNimSum();
if (nimSum == 0) {
return null; // 必败态,无必胜策略
}
for (int i = 0; i < piles.length; i++) {
// 目标:使得该堆操作后,整体异或和变为0
// 新数量 = piles[i] XOR nimSum
int target = piles[i] ^ nimSum;
if (target < piles[i]) {
return new int[]{i, target};
}
}
return null;
}
/**
* 执行移动:从指定堆取石子至目标数量
* @param pileIndex 堆索引
* @param targetAmount 目标剩余数量
* @return 实际取走的石子数
*/
public int makeMove(int pileIndex, int targetAmount) {
if (pileIndex < 0 || pileIndex >= piles.length) {
throw new IllegalArgumentException("无效的堆索引");
}
if (targetAmount < 0 || targetAmount >= piles[pileIndex]) {
throw new IllegalArgumentException("无效的目标数量");
}
int taken = piles[pileIndex] - targetAmount;
piles[pileIndex] = targetAmount;
return taken;
}
/**
* 检查游戏是否结束
*/
public boolean isGameOver() {
for (int pile : piles) {
if (pile > 0) return false;
}
return true;
}
/**
* AI玩家进行最优操作
* @return 操作描述字符串
*/
public String aiMove() {
int[] move = findWinningMove();
if (move != null) {
int taken = makeMove(move[0], move[1]);
return String.format("AI从第%d堆取走%d颗石子,剩余%d颗",
move[0] + 1, taken, move[1]);
} else {
// 必败态:随机取走一颗石子
List<Integer> nonEmpty = new ArrayList<>();
for (int i = 0; i < piles.length; i++) {
if (piles[i] > 0) nonEmpty.add(i);
}
int pileIdx = nonEmpty.get(random.nextInt(nonEmpty.size()));
int taken = random.nextInt(piles[pileIdx]) + 1;
int target = piles[pileIdx] - taken;
makeMove(pileIdx, target);
return String.format("AI(必败态)从第%d堆取走%d颗石子,剩余%d颗",
pileIdx + 1, taken, target);
}
}
Sprague-Grundy函数理论
SG函数是组合博弈论的核心工具。对于每个游戏状态,定义其SG值为所有后继状态SG值的mex(最小非负整数排除值):
$$g(x) = \text{mex}{g(y) \mid y \text{ 是 } x \text{ 的后继状态}}$$
其中 $\text{mex}(S) = \min{k \in \mathbb{N} \mid k \notin S}$
对于Nim的单堆情况,SG函数值恰好等于堆中石子数:$g(n) = n$
/**
* 计算单堆Nim的SG函数值
* 对于标准Nim,SG(n) = n
*/
public static int sgNimSingle(int n) {
return n;
}
/**
* 使用动态规划计算一般单堆游戏的SG函数
* @param maxN 最大堆大小
* @param moves 可执行的操作集合(表示每次可取走的数量)
* @return SG函数值数组
*/
public static int[] computeSgFunction(int maxN, int[] moves) {
int[] sg = new int[maxN + 1];
boolean[] seen = new boolean[maxN + 1];
for (int i = 1; i <= maxN; i++) {
Arrays.fill(seen, false);
// 标记所有后继状态的SG值
for (int move : moves) {
if (i - move >= 0) {
seen[sg[i - move]] = true;
}
}
// mex操作:找最小未出现的非负整数
int g = 0;
while (g <= maxN && seen[g]) {
g++;
}
sg[i] = g;
}
return sg;
}
Nim变种:限制每次取石数量
在Nim的变种中,每次最多只能取k颗石子。这种限制会改变SG函数的计算方式。
/**
* 限制取石数量的Nim游戏
* 每次最多取maxTake颗石子
*/
public static class BoundedNim {
private int[] piles;
private int maxTake;
public BoundedNim(int[] piles, int maxTake) {
this.piles = Arrays.copyOf(piles, piles.length);
this.maxTake = maxTake;
}
/**
* 计算限制取石数量的单堆SG值
* SG(n) = n % (maxTake + 1)
*/
public int calculateBoundedNimSum() {
int xor = 0;
for (int pile : piles) {
xor ^= (pile % (maxTake + 1));
}
return xor;
}
/**
* 寻找必胜移动(限制取石数量版本)
*/
public int[] findWinningMove() {
int nimSum = calculateBoundedNimSum();
if (nimSum == 0) return null;
for (int i = 0; i < piles.length; i++) {
int currentMod = piles[i] % (maxTake + 1);
int targetMod = currentMod ^ nimSum;
// 需要调整取石数量使得新的mod等于targetMod
int diff = currentMod - targetMod;
if (diff > 0 && diff <= maxTake && diff <= piles[i]) {
return new int[]{i, piles[i] - diff};
}
}
return null;
}
}
组合游戏:多子游戏的SG和
Sprague-Grundy定理的关键结论:多个独立子游戏的组合,其SG值等于各子游戏SG值的异或和。这正是Nim游戏中多堆石子异或判定胜负的理论基础。
/**
* 计算多个独立子游戏组合后的SG值
* @param subGameSgs 各子游戏的SG值数组
* @return 组合游戏的SG值
*/
public static int combineSgValues(int[] subGameSgs) {
int combined = 0;
for (int sg : subGameSgs) {
combined ^= sg;
}
return combined;
}
/**
* 适用于复合Nim局面:每堆可能有不同的取石规则
*/
public static class CompositeNim {
private List<NimPile> piles;
static class NimPile {
int stones;
int[] allowedMoves; // 该堆允许的取石数量
NimPile(int stones, int[] allowedMoves) {
this.stones = stones;
this.allowedMoves = allowedMoves;
}
}
public CompositeNim(List<NimPile> piles) {
this.piles = piles;
}
/**
* 计算整个局面的SG值
* 每堆独立计算SG后异或
*/
public int evaluate() {
int totalXor = 0;
for (NimPile pile : piles) {
int maxN = pile.stones;
int[] sgTable = computeSgFunction(maxN, pile.allowedMoves);
totalXor ^= sgTable[maxN];
}
return totalXor;
}
}
完整可运行对战程序
/**
* 运行人机对战
*/
public void playVsAI() {
Scanner scanner = new Scanner(System.in);
boolean playerTurn = true; // true=玩家先手
System.out.println("=== Nim游戏 ===");
System.out.println("初始石子堆: " + Arrays.toString(piles));
System.out.println("初始Nim和: " + calculateNimSum());
System.out.println();
while (!isGameOver()) {
System.out.println("当前石子堆: " + Arrays.toString(piles));
if (playerTurn) {
System.out.print("选择堆(1-" + piles.length + "): ");
int pile = scanner.nextInt() - 1;
System.out.print("取走后剩余数量: ");
int target = scanner.nextInt();
try {
int taken = makeMove(pile, target);
System.out.println("你取走了" + taken + "颗石子");
} catch (Exception e) {
System.out.println("非法操作!" + e.getMessage());
continue;
}
} else {
String action = aiMove();
System.out.println("AI行动: " + action);
}
playerTurn = !playerTurn;
System.out.println("Nim和: " + calculateNimSum());
System.out.println();
}
System.out.println(playerTurn ? "AI获胜!" : "你获胜了!");
}
public static void main(String[] args) {
// 经典局面:3堆石子 (3, 4, 5)
// Nim和 = 3 XOR 4 XOR 5 = 2,为必胜态
int[] initial = {3, 4, 5};
NimGame game = new NimGame(initial);
game.playVsAI();
}
}
SG函数计算的可视化示例
以限制每次最多取3颗石子的Nim为例,SG值计算如下:
| 堆大小n | 后继状态SG集合 | mex运算 | SG(n) |
|---|---|---|---|
| 0 | ∅ | mex(∅) = 0 | 0 |
| 1 | {SG(0)} = {0} | mex({0}) = 1 | 1 |
| 2 | {SG(1), SG(0)} = {1, 0} | mex({0,1}) = 2 | 2 |
| 3 | {SG(2), SG(1), SG(0)} = {2, 1, 0} | mex({0,1,2}) = 3 | 3 |
| 4 | {SG(3), SG(2), SG(1)} = {3, 2, 1} | mex({1,2,3}) = 0 | 0 |
| 5 | {SG(4), SG(3), SG(2)} = {0, 3, 2} | mex({0,2,3}) = 1 | 1 |
可见SG值呈现周期为4的循环:$SG(n) = n \bmod 4$
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| Nim和计算 | O(n) | O(1) |
| 必胜策略查找 | O(n) | O(1) |
| SG函数表构建 | O(maxN × |moves|) | O(maxN) |
| 多子游戏组合评估 | O(k × maxN × |moves|) | O(k × maxN) |
总结
Nim游戏是理解组合博弈论的绝佳入口。通过异或和判定胜负、SG函数刻画状态价值、Sprague-Grundy定理组合子游戏,我们构建了一套完整的博弈分析框架。Java实现中关键点在于:利用位运算高效计算nim-sum、通过mex运算构建SG函数表、将复杂局面分解为独立子问题后组合求解。这些思想不仅适用于Nim游戏,也是分析更复杂组合博弈(如Kayles、Grundy游戏)的基础工具。