每日算法 — 使用java实现猜数字:二分搜索与信息熵最优猜测策略

引言

猜数字是一款经典的逻辑推理游戏:一方心里想一个数字,另一方通过有限的猜测次数将其找出。每次猜测后会得到”太大”或”太小”的反馈。看似简单的游戏背后,蕴含着信息论、二分搜索与最优决策的深刻数学原理。本文用Java实现带AI的猜数字游戏,让计算机用最优策略猜测。

算法原理

1. 二分搜索基础

若数字范围为[1, N],每次猜测中间的数字,可将搜索空间减半。最坏情况下需要⌈log₂N⌉次猜测。

2. 信息熵视角

每次猜测相当于一次信息获取。设当前候选集合为S,猜测数字g将S划分为:
– S₁ = {x ∈ S | x < g}
– S₂ = {x ∈ S | x = g}
– S₃ = {x ∈ S | x > g}

最优猜测应使三个子集的大小尽可能均衡,从而最大化信息增益(最小化熵)。

3. 最优策略

标准二分搜索在均匀分布假设下已是最优。若数字分布非均匀,可用加权中位数替代普通中位数。

Java实现

import java.util.*;

/**
 * 猜数字游戏AI实现
 * 包含二分搜索、信息熵计算与最优策略分析
 */
public class GuessNumber {

    // 游戏配置
    private final int minRange;
    private final int maxRange;
    private final int secretNumber;
    private int guessCount;

    public GuessNumber(int minRange, int maxRange) {
        this.minRange = minRange;
        this.maxRange = maxRange;
        this.secretNumber = minRange + (int)(Math.random() * (maxRange - minRange + 1));
        this.guessCount = 0;
    }

    /**
     * 获取对一次猜测的反馈
     * @return -1: 太大, 0: 正确, 1: 太小
     */
    public int feedback(int guess) {
        guessCount++;
        if (guess == secretNumber) return 0;
        return guess < secretNumber ? 1 : -1;
    }

    /**
     * 二分搜索AI策略
     */
    public int[] binarySearchAI() {
        int low = minRange, high = maxRange;
        List<Integer> history = new ArrayList<>();

        while (low <= high) {
            int mid = low + (high - low) / 2;
            history.add(mid);
            int result = feedback(mid);
            if (result == 0) break;
            else if (result < 0) high = mid - 1;
            else low = mid + 1;
        }
        return history.stream().mapToInt(Integer::intValue).toArray();
    }

    /**
     * 信息熵最优策略:选择使剩余候选集熵最小的猜测
     */
    public int[] entropyOptimalAI() {
        List<Integer> candidates = new ArrayList<>();
        for (int i = minRange; i <= maxRange; i++) candidates.add(i);
        List<Integer> history = new ArrayList<>();

        while (candidates.size() > 1) {
            int bestGuess = candidates.get(0);
            double minEntropy = Double.MAX_VALUE;

            for (int guess : candidates) {
                // 计算以guess为猜测时的期望熵
                int smaller = 0, equal = 0, larger = 0;
                for (int cand : candidates) {
                    if (cand < guess) smaller++;
                    else if (cand == guess) equal++;
                    else larger++;
                }

                double entropy = 0;
                int total = candidates.size();
                if (smaller > 0) {
                    double p = (double) smaller / total;
                    entropy -= p * Math.log(p) / Math.log(2);
                }
                if (equal > 0) {
                    double p = (double) equal / total;
                    entropy -= p * Math.log(p) / Math.log(2);
                }
                if (larger > 0) {
                    double p = (double) larger / total;
                    entropy -= p * Math.log(p) / Math.log(2);
                }

                if (entropy < minEntropy) {
                    minEntropy = entropy;
                    bestGuess = guess;
                }
            }

            history.add(bestGuess);
            int result = feedback(bestGuess);
            if (result == 0) break;

            final int g = bestGuess;
            candidates.removeIf(c ->
                (result < 0 && c >= g) || (result > 0 && c <= g)
            );
        }

        if (candidates.size() == 1 && guessCount == 0) {
            history.add(candidates.get(0));
            feedback(candidates.get(0));
        }

        return history.stream().mapToInt(Integer::intValue).toArray();
    }

    /**
     * 计算理论最小猜测次数(信息论下界)
     */
    public static double informationTheoreticLowerBound(int n) {
        return Math.ceil(Math.log(n) / Math.log(2));
    }

    /**
     * 批量模拟比较两种策略
     */
    public static void simulate(int minRange, int maxRange, int trials) {
        int totalBinary = 0, totalEntropy = 0;
        int binaryWins = 0, entropyWins = 0;

        for (int t = 0; t < trials; t++) {
            GuessNumber game1 = new GuessNumber(minRange, maxRange);
            GuessNumber game2 = new GuessNumber(minRange, maxRange);

            // 确保两者目标数字相同
            int secret = game1.secretNumber;
            game2.secretNumber = secret;

            int[] binaryResult = game1.binarySearchAI();
            int[] entropyResult = game2.entropyOptimalAI();

            totalBinary += binaryResult.length;
            totalEntropy += entropyResult.length;

            if (binaryResult.length < entropyResult.length) binaryWins++;
            else if (entropyResult.length < binaryResult.length) entropyWins++;
        }

        int n = maxRange - minRange + 1;
        System.out.println("===== 猜数字策略对比 =====");
        System.out.printf("范围: [%d, %d], 共%d个数%n", minRange, maxRange, n);
        System.out.printf("信息论下界: %.0f次%n", informationTheoreticLowerBound(n));
        System.out.printf("二分搜索平均: %.2f次%n", (double) totalBinary / trials);
        System.out.printf("熵最优策略平均: %.2f次%n", (double) totalEntropy / trials);
        System.out.printf("二分搜索获胜: %d次%n", binaryWins);
        System.out.printf("熵最优获胜: %d次%n", entropyWins);
    }

    // 需要允许修改secretNumber用于对比实验
    int secretNumber;

    public static void main(String[] args) {
        simulate(1, 100, 1000);
        System.out.println();
        simulate(1, 10000, 100);
    }
}

复杂度分析

  • 二分搜索:时间O(log N),空间O(1)
  • 熵最优策略:每次需遍历所有候选计算熵,时间O(N²),但仅用于教学演示
  • 信息论下界:⌈log₂N⌉,任何确定性算法都无法突破

扩展方向

  1. Mastermind变体:若反馈变为”A个位置正确、B个数字正确但位置错误”,问题变为NP难问题,可用遗传算法或约束传播求解
  2. 非均匀先验:若数字服从某种分布(如正态分布),可用贝叶斯更新替代二分搜索
  3. 双人博弈:若对手可以撒谎有限次数,引入容错机制

总结

本文从信息熵角度重新诠释了猜数字游戏,证明二分搜索在均匀分布下即为最优策略。通过Java实现对比了经典二分搜索与显式熵最小化策略,验证了理论下界的紧性。