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

游戏简介与算法思路

石头剪刀布是人类最古老、最普遍的博弈游戏之一。看似纯粹靠运气,但心理学研究表明,人类玩家在出招时并非完全随机,而是存在可预测的模式:赢了倾向于保持、输了倾向于改变,甚至会在连续出相同手势后切换。本文用Java实现一个带AI对手的石头剪刀布游戏,核心算法包含三层预测策略:马尔可夫链状态转移预测玩家下一手、频率分析捕捉长期偏好、纳什均衡混合策略作为最终保底,让AI真正具备”读心”能力。

核心算法一:马尔可夫链状态转移

马尔可夫链的核心假设是:下一状态的概率仅依赖于当前状态。在石头剪刀布中,如果玩家上一轮出了”石头”,本轮出”剪刀”的条件概率可能显著高于1/3。我们维护一个3×3的转移计数矩阵,记录从手势A到手势B的历史出现次数:

         石头   剪刀   布
石头     [c00   c01   c02]
剪刀     [c10   c11   c12]
布       [c20   c21   c22]

每当玩家出一轮后,更新对应转移计数。预测时,根据玩家上一轮出的手势,选择转移计数最高的下一手势作为预测目标,AI则出能克制该预测的招式。

核心算法二:频率分析与混合策略

当历史数据不足或马尔可夫预测置信度较低时,系统回退到频率分析层:统计玩家各手势的历史出现频次,选择出现频率最高的手势进行反制。若连频率分析也无法提供有效信号(如完全均匀分布),AI则采用纳什均衡混合策略——完全随机出招,确保长期期望收益不为负。

Java完整实现

以下代码提供可直接运行的完整项目,包含预测引擎、游戏循环与统计模块:

import java.util.*;

/**
 * 手势枚举
 */
enum Move {
    ROCK(0, "石头"), SCISSORS(1, "剪刀"), PAPER(2, "布");
    final int idx;
    final String label;
    Move(int idx, String label) { this.idx = idx; this.label = label; }
    /**
     * 判断本手势是否击败对手
     */
    boolean beats(Move other) {
        return (this.idx + 1) % 3 == other.idx; // 石头赢剪刀, 剪刀赢布, 布赢石头
    }
    static Move fromIdx(int i) { return values()[i]; }
}

/**
 * 马尔可夫链预测器:基于状态转移矩阵预测玩家下一手
 */
class MarkovPredictor {
    private final int[][] trans = new int[3][3]; // trans[当前][下一]计数
    private Move lastMove = null;
    private int totalTrans = 0;

    void record(Move move) {
        if (lastMove != null) {
            trans[lastMove.idx][move.idx]++;
            totalTrans++;
        }
        lastMove = move;
    }

    /**
     * 返回预测的玩家下一手;若无足够数据则返回null
     */
    Move predict() {
        if (lastMove == null || totalTrans < 3) return null;
        int[] row = trans[lastMove.idx];
        int max = -1, best = -1;
        for (int i = 0; i < 3; i++) {
            if (row[i] > max) { max = row[i]; best = i; }
        }
        return best >= 0 ? Move.fromIdx(best) : null;
    }

    double confidence() {
        if (lastMove == null || totalTrans == 0) return 0;
        int[] row = trans[lastMove.idx];
        int sum = Arrays.stream(row).sum();
        if (sum == 0) return 0;
        int max = Arrays.stream(row).max().orElse(0);
        return (double) max / sum;
    }
}

/**
 * 频率分析器:统计玩家历史出招频率
 */
class FrequencyAnalyzer {
    private final int[] freq = new int[3];
    private int total = 0;

    void record(Move move) {
        freq[move.idx]++;
        total++;
    }

    Move predict() {
        if (total == 0) return null;
        int max = -1, best = -1;
        for (int i = 0; i < 3; i++) {
            if (freq[i] > max) { max = freq[i]; best = i; }
        }
        return best >= 0 ? Move.fromIdx(best) : null;
    }

    double confidence() {
        if (total == 0) return 0;
        int max = Arrays.stream(freq).max().orElse(0);
        return (double) max / total;
    }
}

/**
 * AI决策引擎:三层策略融合
 */
class AiEngine {
    private final MarkovPredictor markov = new MarkovPredictor();
    private final FrequencyAnalyzer freq = new FrequencyAnalyzer();
    private final Random rand = new Random();

    Move decide() {
        Move predicted = null;
        double conf = 0;

        // 第一层:马尔可夫链预测
        Move m = markov.predict();
        if (m != null && markov.confidence() > 0.45) {
            predicted = m;
            conf = markov.confidence();
        }

        // 第二层:频率分析回退
        if (predicted == null) {
            Move f = freq.predict();
            if (f != null && freq.confidence() > 0.40) {
                predicted = f;
                conf = freq.confidence();
            }
        }

        // 第三层:纳什均衡随机策略
        if (predicted == null) {
            return Move.fromIdx(rand.nextInt(3));
        }

        // 出能克制predicted的招式
        return Move.fromIdx((predicted.idx + 2) % 3); // predicted=石头→布, 剪刀→石头, 布→剪刀
    }

    void learn(Move playerMove) {
        markov.record(playerMove);
        freq.record(playerMove);
    }
}

/**
 * 游戏主控与统计
 */
public class RockPaperScissors {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        AiEngine ai = new AiEngine();
        int wins = 0, losses = 0, draws = 0;

        System.out.println("=== 石头剪刀布 AI对战 ===");
        System.out.println("输入: 0=石头 1=剪刀 2=布 其他=退出");

        while (true) {
            System.out.print("\n请出招: ");
            String in = sc.nextLine().trim();
            if (!in.matches("[012]")) break;

            Move player = Move.fromIdx(Integer.parseInt(in));
            Move aiMove = ai.decide();
            ai.learn(player); // AI学习本轮玩家行为

            System.out.printf("你出 %s, AI出 %s", player.label, aiMove.label);
            if (player.beats(aiMove)) {
                System.out.println(" → 你赢了!");
                wins++;
            } else if (aiMove.beats(player)) {
                System.out.println(" → AI赢了!");
                losses++;
            } else {
                System.out.println(" → 平局");
                draws++;
            }
            int total = wins + losses + draws;
            System.out.printf("当前战绩: 胜%d 负%d 平%d (胜率%.1f%%)%n",
                wins, losses, draws, total > 0 ? 100.0 * wins / total : 0);
        }
        System.out.println("游戏结束,感谢对战!");
    }
}

算法策略详解

马尔可夫链层在数据量达到3轮后开始生效。当玩家连续两次出”石头”后第三次仍出”石头”的概率若被观测到偏高,矩阵会累积这一偏好,AI下一次在玩家出”石头”后将倾向于出”布”进行反制。置信度阈值设为0.45,避免在数据稀疏时盲目跟从噪声。

频率分析层捕捉的是长期偏好而非相邻轮次的依赖关系。部分玩家潜意识中偏爱某一种手势(如习惯性出”石头”),频率层能在马尔可夫链无显著信号时提供稳定预测。

纳什均衡层是理论保底。当玩家出招完全随机且均匀时,任何确定性策略的长期期望收益都不超过0。此时AI退化为均匀随机,避免被对手利用自身规律。这也体现了博弈论中混合策略均衡的核心思想:当对手无法被预测时,最优应对就是让自己也无法被预测。

复杂度分析

  • 时间复杂度:每轮决策和更新均为 O(1)。马尔可夫矩阵查询和频率数组遍历都是固定3个元素,与对局总轮数无关。
  • 空间复杂度:O(1)。仅需维护 3×3 转移矩阵和两个长度为3的数组,不随数据量增长。

扩展方向

  1. 高阶马尔可夫链:将状态从单轮扩展为最近两轮的组合(9种状态),捕捉更长的序列模式。
  2. 指数衰减权重:给近期转移更高权重,让AI更快适应玩家策略变化。
  3. 强化学习融合:用Q-Learning动态调整三层策略的切换阈值,根据实际胜率在线优化。

总结

本文用Java实现了一个具备三层预测策略的石头剪刀布AI。马尔可夫链挖掘相邻轮次的转移规律,频率分析捕捉长期偏好,纳什均衡随机策略确保理论下限。整个实现仅百余行代码,却完整展示了从概率建模到博弈决策的算法思维——这正是算法魅力的所在:用简洁的逻辑,在不确定性中寻找优势。