猜数字(Bulls and Cows / Mastermind变体)是一款经典的逻辑推理游戏:系统生成一个不含重复数字的N位数,玩家每次猜测后会得到反馈——”A”表示数字和位置都正确,”B”表示数字正确但位置错误。本文用Java实现一个AI猜数字求解器,核心讲解如何利用二分搜索思想与信息论熵减策略,以最少次数猜中目标。
信息论建模
猜数字的本质是从候选解空间中寻找目标。对于4位不重复数字(0-9),候选空间大小为10×9×8×7=5040。每次猜测的反馈(如1A2B)将空间划分为多个子集,AI的目标是让每次猜测后剩余候选数最少——这正是信息增益最大化问题。
信息论告诉我们:最优猜测是让各种反馈结果尽可能均衡分布,从而使期望信息增益最大。Shannon熵定义为H = -Σpᵢlog₂(pᵢ),均衡分布时熵最大,不确定性消除最多。
两种策略实现
策略一:最小最大(Knuth算法)
对每一个可能的猜测,模拟所有可能的反馈结果,计算最坏情况下剩余候选数的最大值,选择这个最大值最小的猜测。
策略二:熵最大化(信息论最优)
计算每个猜测的反馈分布熵,选择期望熵最大的猜测,让每次猜测平均消除最多的不确定性。
Java 实现
import java.util.*;
public class GuessNumberAI {
private final int digits; // 数字位数
private final boolean allowDuplicate;
private List<String> candidates = new ArrayList<>();
private Random rand = new Random();
public GuessNumberAI(int digits, boolean allowDuplicate) {
this.digits = digits;
this.allowDuplicate = allowDuplicate;
generateAllCandidates();
}
// 生成所有可能的候选解
private void generateAllCandidates() {
generateCandidates(new StringBuilder(), new boolean[10]);
}
private void generateCandidates(StringBuilder sb, boolean[] used) {
if (sb.length() == digits) {
candidates.add(sb.toString());
return;
}
for (int d = 0; d <= 9; d++) {
if (!allowDuplicate && used[d]) continue;
sb.append(d);
used[d] = true;
generateCandidates(sb, used);
used[d] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
// 计算猜测与目标的反馈 (A,B)
public static int[] calculateFeedback(String guess, String target) {
int a = 0, b = 0;
boolean[] guessUsed = new boolean[digits];
boolean[] targetUsed = new boolean[digits];
// 先统计位置正确的A
for (int i = 0; i < digits; i++) {
if (guess.charAt(i) == target.charAt(i)) {
a++; guessUsed[i] = targetUsed[i] = true;
}
}
// 再统计数字正确但位置错误的B
for (int i = 0; i < digits; i++) {
if (guessUsed[i]) continue;
for (int j = 0; j < digits; j++) {
if (targetUsed[j]) continue;
if (guess.charAt(i) == target.charAt(j)) {
b++; guessUsed[i] = targetUsed[j] = true; break;
}
}
}
return new int[]{a, b};
}
// 根据反馈过滤候选解
public void filterCandidates(String guess, int a, int b) {
List<String> filtered = new ArrayList<>();
for (String c : candidates) {
int[] fb = calculateFeedback(guess, c);
if (fb[0] == a && fb[1] == b) filtered.add(c);
}
candidates = filtered;
}
// 最小最大策略:选择最坏情况下剩余候选数最少的猜测
public String minmaxGuess() {
String bestGuess = candidates.get(0);
int minMaxRemaining = Integer.MAX_VALUE;
for (String guess : candidates) {
Map<String, Integer> distribution = new HashMap<>();
for (String target : candidates) {
int[] fb = calculateFeedback(guess, target);
String key = fb[0] + "A" + fb[1] + "B";
distribution.merge(key, 1, Integer::sum);
}
int maxRemaining = Collections.max(distribution.values());
if (maxRemaining < minMaxRemaining) {
minMaxRemaining = maxRemaining;
bestGuess = guess;
}
}
return bestGuess;
}
// 熵最大化策略:选择期望信息增益最大的猜测
public String entropyGuess() {
String bestGuess = candidates.get(0);
double maxEntropy = -1;
for (String guess : candidates) {
Map<String, Integer> distribution = new HashMap<>();
for (String target : candidates) {
int[] fb = calculateFeedback(guess, target);
String key = fb[0] + "A" + fb[1] + "B";
distribution.merge(key, 1, Integer::sum);
}
double entropy = 0;
int total = candidates.size();
for (int count : distribution.values()) {
double p = (double) count / total;
entropy -= p * Math.log(p) / Math.log(2);
}
if (entropy > maxEntropy) {
maxEntropy = entropy;
bestGuess = guess;
}
}
return bestGuess;
}
// 模拟AI猜测过程
public int solve(String target, boolean useEntropy) {
candidates.clear();
generateAllCandidates();
int steps = 0;
String guess = "0123".substring(0, Math.min(digits, 4)); // 初始猜测
if (digits == 4) guess = "0123"; // 经典4位猜数字优化初始值
while (true) {
steps++;
int[] fb = calculateFeedback(guess, target);
System.out.println("Step " + steps + ": guess " + guess + " -> " + fb[0] + "A" + fb[1] + "B");
if (fb[0] == digits) break;
filterCandidates(guess, fb[0], fb[1]);
System.out.println(" Remaining candidates: " + candidates.size());
guess = useEntropy ? entropyGuess() : minmaxGuess();
}
return steps;
}
public static void main(String[] args) {
GuessNumberAI ai = new GuessNumberAI(4, false);
String target = "5283"; // 目标数字
System.out.println("=== Minimax Strategy ===");
int steps1 = ai.solve(target, false);
System.out.println("Solved in " + steps1 + " steps\n");
System.out.println("=== Entropy Strategy ===");
int steps2 = ai.solve(target, true);
System.out.println("Solved in " + steps2 + " steps");
}
}
复杂度分析
- 时间复杂度:每次猜测需要O(C²)计算所有候选对的反馈分布,C为当前候选数。4位数字通常5-6步内解决,每步最多5040²≈2500万次比较,实际可用哈希优化。
- 空间复杂度:O(C)存储候选解列表。
策略对比与最优性
对于标准4位猜数字(数字不重复):
| 策略 | 平均步数 | 最坏步数 | 计算开销 |
|---|---|---|---|
| 随机猜测 | ~5.2 | 9+ | 极低 |
| 最小最大 | ~4.47 | 5 | 高 |
| 熵最大化 | ~4.42 | 6 | 高 |
最小最大策略保证最坏情况最优(Knuth证明5步内必解),而熵最大化策略在平均表现上略优。两者均远优于人类玩家的直觉猜测。
扩展应用
猜数字的信息论框架可推广至:Wordle(5字母单词猜测)、Mastermind(颜色 peg 组合)、以及各类主动学习场景——AI通过提问逐步缩小假设空间,这在医学诊断、故障排查等决策系统中有着广泛应用。