Mastermind(色彩密码)是一款经典的逻辑推理桌游:一方设置一个由4个彩色 peg 组成的秘密代码,另一方通过有限次数的猜测来破解它。每次猜测后,会获得两种反馈——黑色 peg(颜色和位置都正确)和白色 peg(颜色正确但位置错误)。1977年,Donald Knuth 证明了该游戏可以在最多5次猜测内解决。本文将用Java实现 Mastermind 的AI求解器,深入讲解 Knuth 的最小最大算法与信息论视角下的最优猜测策略。
一、游戏规则与状态空间
秘密代码由4个位置组成,每个位置可以是6种颜色之一(允许重复)。总状态空间为 6⁴ = 1296 种可能。
public class MastermindConfig {
public static final int CODE_LENGTH = 4;
public static final int NUM_COLORS = 6;
public static final char[] COLORS = {'R', 'G', 'B', 'Y', 'W', 'K'}; // 红绿蓝黄白黑
}
二、反馈计算
对于猜测代码 G 和秘密代码 S,反馈 (black, white) 的计算逻辑如下:
public class Feedback {
private final int black; // 位置颜色都正确
private final int white; // 颜色正确位置错误
public static Feedback evaluate(char[] guess, char[] secret) {
int black = 0;
int white = 0;
boolean[] guessUsed = new boolean[CODE_LENGTH];
boolean[] secretUsed = new boolean[CODE_LENGTH];
// 先统计 black(位置和颜色都匹配)
for (int i = 0; i < CODE_LENGTH; i++) {
if (guess[i] == secret[i]) {
black++;
guessUsed[i] = true;
secretUsed[i] = true;
}
}
// 再统计 white(颜色匹配但位置不同)
for (int i = 0; i < CODE_LENGTH; i++) {
if (guessUsed[i]) continue;
for (int j = 0; j < CODE_LENGTH; j++) {
if (secretUsed[j]) continue;
if (guess[i] == secret[j]) {
white++;
secretUsed[j] = true;
break;
}
}
}
return new Feedback(black, white);
}
}
三、Knuth 最小最大算法
Knuth 算法的核心思想是:每次猜测后,根据反馈将所有可能的秘密代码划分为若干等价类。选择能够将最大等价类最小化的猜测,从而保证无论得到什么反馈,剩余的可能性都尽可能少。
public class KnuthSolver {
private List<char[]> possibleCodes; // 当前所有可能的秘密代码
private List<char[]> allCodes; // 全部1296种代码
public KnuthSolver() {
allCodes = generateAllCodes();
possibleCodes = new ArrayList<>(allCodes);
}
public char[] makeGuess() {
if (possibleCodes.size() == 1) {
return possibleCodes.get(0); // 唯一解
}
// 特殊情况:第一次猜测固定为 AABC 模式(最优首猜)
if (possibleCodes.size() == allCodes.size()) {
return new char[]{'R', 'R', 'G', 'B'}; // 1123 模式
}
char[] bestGuess = null;
int minMaxSize = Integer.MAX_VALUE;
// 遍历所有可能的猜测(不仅限于 possibleCodes,也包括已排除的代码)
for (char[] guess : allCodes) {
// 计算该猜测下,各种反馈对应的等价类大小
Map<Feedback, Integer> partitionSizes = new HashMap<>();
for (char[] possible : possibleCodes) {
Feedback fb = Feedback.evaluate(guess, possible);
partitionSizes.merge(fb, 1, Integer::sum);
}
// 最小最大准则:关注最大等价类的大小
int maxPartition = partitionSizes.values().stream()
.max(Integer::compare)
.orElse(0);
// 优先选择可能解空间内的猜测,其次才选非可能解的猜测
boolean isPossible = isInPossibleCodes(guess);
// 更新最优猜测
if (maxPartition < minMaxSize ||
(maxPartition == minMaxSize && isPossible && !isInPossibleCodes(bestGuess))) {
minMaxSize = maxPartition;
bestGuess = guess.clone();
}
}
return bestGuess;
}
public void update(Feedback feedback, char[] guess) {
// 根据反馈过滤可能的秘密代码
possibleCodes.removeIf(code -> !Feedback.evaluate(guess, code).equals(feedback));
}
}
四、信息论视角:熵最大化策略
除了最小最大准则,还可以从信息论角度选择每次猜测——最大化期望信息增益(即最小化条件熵)。
public char[] makeEntropyGuess() {
char[] bestGuess = null;
double maxEntropy = -1;
for (char[] guess : allCodes) {
Map<Feedback, Integer> partition = new HashMap<>();
for (char[] possible : possibleCodes) {
Feedback fb = Feedback.evaluate(guess, possible);
partition.merge(fb, 1, Integer::sum);
}
// 计算该猜测的期望信息增益(熵)
double entropy = 0.0;
int total = possibleCodes.size();
for (int count : partition.values()) {
if (count > 0) {
double p = (double) count / total;
entropy -= p * Math.log(p) / Math.log(2);
}
}
if (entropy > maxEntropy) {
maxEntropy = entropy;
bestGuess = guess.clone();
}
}
return bestGuess;
}
五、求解器主循环
public class MastermindGame {
public static void solve(char[] secret) {
KnuthSolver solver = new KnuthSolver();
int attempts = 0;
while (true) {
char[] guess = solver.makeGuess();
attempts++;
Feedback fb = Feedback.evaluate(guess, secret);
System.out.printf("Attempt %d: %s -> %s%n",
attempts, new String(guess), fb);
if (fb.getBlack() == MastermindConfig.CODE_LENGTH) {
System.out.println("Solved in " + attempts + " attempts!");
break;
}
solver.update(fb, guess);
}
}
}
六、性能对比
| 策略 | 平均猜测次数 | 最坏情况猜测次数 |
|---|---|---|
| 随机猜测 | ~4.5 | >10 |
| 最小最大(Knuth) | 4.34 | 5 |
| 熵最大化 | 4.20 | 6 |
Knuth 的最小最大算法保证在5步内解决,而熵最大化策略平均步数更少但最坏情况略差。
七、总结
Mastermind 求解器展示了约束传播与信息论在组合搜索中的威力。Knuth 的最小最大算法通过系统性地评估每个猜测的”最坏情况表现”来保证求解效率,而熵最大化策略则在平均意义上更优。两种方法的核心都是将庞大的状态空间通过反馈逐步缩小,这与现代约束满足问题(CSP)的求解思路一脉相承。