每日算法 — 使用java实现猜拳:马尔可夫链状态预测与频率自适应对抗策略

引言

猜拳(石头剪刀布)是人类最古老的游戏之一,看似简单却蕴含丰富的博弈论与概率学原理。当对手不是完全随机出拳时,其行为往往存在可预测的模式。本文将用Java实现一个智能猜拳AI,通过马尔可夫链建模对手出拳序列的状态转移规律,并结合频率统计进行自适应对抗,让AI在有限轮次内逐步占据优势。

核心算法思路

马尔可夫链状态转移

马尔可夫链的核心假设是:下一时刻的状态仅依赖于当前时刻的状态。在猜拳场景中,我们可以将对手上一次出的拳作为当前状态,预测其下一次最可能出的拳。具体实现时,维护一个 3×3 的转移计数矩阵,记录从每一种拳到另一种拳的转移次数。

频率自适应策略

单独使用马尔可夫链可能在对手出拳模式变化时反应滞后。因此引入频率统计作为辅助:记录对手历史出拳中石头、剪刀、布各自出现的次数,选择出现频率最高的拳的克制拳作为备选策略。最终决策通过加权融合马尔可夫预测与频率统计的结果。

反制逻辑

一旦预测出对手最可能出的拳,AI选择能够克制该拳的选项:石头克剪刀、剪刀克布、布克石头。

完整Java实现

import java.util.*;

/**
 * 智能猜拳AI —— 基于马尔可夫链与频率统计的自适应对抗
 */
public class SmartRPS {

    // 出拳枚举:0=石头, 1=剪刀, 2=布
    public enum Move { ROCK, SCISSORS, PAPER }

    // 马尔可夫转移计数矩阵:transCount[prev][next]
    private final int[][] transCount = new int[3][3];

    // 频率统计
    private final int[] freqCount = new int[3];

    // 历史出拳记录
    private final List<Move> history = new ArrayList<>();

    // 权重参数
    private static final double MARKOV_WEIGHT = 0.7;
    private static final double FREQ_WEIGHT = 0.3;

    /**
     * 将Move转为索引
     */
    private int idx(Move m) {
        return m.ordinal();
    }

    /**
     * 获取克制指定出拳的选项
     */
    private Move counter(Move m) {
        return switch (m) {
            case ROCK -> Move.PAPER;      // 布克石头
            case SCISSORS -> Move.ROCK;   // 石头克剪刀
            case PAPER -> Move.SCISSORS;  // 剪刀克布
        };
    }

    /**
     * 记录对手的出拳,更新马尔可夫矩阵与频率统计
     */
    public void recordOpponentMove(Move move) {
        int curIdx = idx(move);
        freqCount[curIdx]++;

        if (!history.isEmpty()) {
            int prevIdx = idx(history.get(history.size() - 1));
            transCount[prevIdx][curIdx]++;
        }
        history.add(move);
    }

    /**
     * 基于马尔可夫链预测对手下一次出拳
     */
    private double[] markovPredict() {
        double[] prob = new double[3];
        if (history.isEmpty()) {
            Arrays.fill(prob, 1.0 / 3);
            return prob;
        }
        int lastIdx = idx(history.get(history.size() - 1));
        int total = Arrays.stream(transCount[lastIdx]).sum();
        if (total == 0) {
            Arrays.fill(prob, 1.0 / 3);
            return prob;
        }
        for (int i = 0; i < 3; i++) {
            prob[i] = (double) transCount[lastIdx][i] / total;
        }
        return prob;
    }

    /**
     * 基于频率统计预测对手下一次出拳
     */
    private double[] freqPredict() {
        double[] prob = new double[3];
        int total = Arrays.stream(freqCount).sum();
        if (total == 0) {
            Arrays.fill(prob, 1.0 / 3);
            return prob;
        }
        for (int i = 0; i < 3; i++) {
            prob[i] = (double) freqCount[i] / total;
        }
        return prob;
    }

    /**
     * 综合预测并选择AI出拳
     */
    public Move chooseMove() {
        double[] markov = markovPredict();
        double[] freq = freqPredict();
        double[] combined = new double[3];
        for (int i = 0; i < 3; i++) {
            combined[i] = MARKOV_WEIGHT * markov[i] + FREQ_WEIGHT * freq[i];
        }

        // 选择概率最高的对手出拳,并反制
        int bestOpponent = 0;
        for (int i = 1; i < 3; i++) {
            if (combined[i] > combined[bestOpponent]) {
                bestOpponent = i;
            }
        }
        return counter(Move.values()[bestOpponent]);
    }

    /**
     * 判定单轮胜负:1=AI胜, -1=对手胜, 0=平局
     */
    public int judge(Move ai, Move opponent) {
        if (ai == opponent) return 0;
        return (idx(ai) - idx(opponent) + 3) % 3 == 1 ? 1 : -1;
    }

    /**
     * 运行模拟对战
     */
    public static void main(String[] args) {
        SmartRPS ai = new SmartRPS();
        Random rand = new Random(42);
        int aiWins = 0, oppWins = 0, draws = 0;

        // 模拟对手:前30轮偏好石头,之后偏好剪刀
        for (int round = 1; round <= 100; round++) {
            Move opponentMove;
            if (round <= 30) {
                opponentMove = rand.nextDouble() < 0.6 ? Move.ROCK :
                               (rand.nextBoolean() ? Move.SCISSORS : Move.PAPER);
            } else {
                opponentMove = rand.nextDouble() < 0.6 ? Move.SCISSORS :
                               (rand.nextBoolean() ? Move.ROCK : Move.PAPER);
            }

            Move aiMove = ai.chooseMove();
            int result = ai.judge(aiMove, opponentMove);

            if (result > 0) aiWins++;
            else if (result < 0) oppWins++;
            else draws++;

            ai.recordOpponentMove(opponentMove);
        }

        System.out.println("=== 100轮对战结果 ===");
        System.out.println("AI胜: " + aiWins);
        System.out.println("对手胜: " + oppWins);
        System.out.println("平局: " + draws);
        System.out.println("AI胜率: " + (aiWins * 100.0 / (aiWins + oppWins)) + "%");
    }
}

复杂度分析

  • 时间复杂度:每轮预测和记录操作均为 $O(1)$,因为马尔可夫矩阵和频率数组大小固定为常数。
  • 空间复杂度:$O(n)$,其中 $n$ 为历史记录长度(主要用于存储历史出拳序列,核心矩阵仅占用常数空间)。

扩展方向

  1. 高阶马尔可夫链:使用前两次出拳作为状态(3×3×3 转移矩阵),捕捉更长的序列模式。
  2. 动态权重调整:根据近期预测准确率自动调整 MARKOV_WEIGHT 与 FREQ_WEIGHT。
  3. 对手模型切换检测:当预测准确率连续下降时,触发模型重置或切换为随机策略。

总结

本文通过马尔可夫链与频率统计的双通道预测,为猜拳游戏构建了一个简单却有效的自适应AI。核心思想在于:任何非完全随机的行为序列都蕴含可挖掘的统计规律,而马尔可夫链正是捕捉这类短时序依赖的利器。读者可将此框架迁移到其他序列预测场景,如用户行为预测、股票趋势分析等。