每日算法 — 使用java实现Memory:记忆匹配与洗牌算法

引言

Memory(记忆翻牌、配对游戏)是经典的卡牌益智游戏。玩家每次翻开两张卡牌,若图案相同则消除,不同则翻回,目标是凭记忆力在最短步数内消除所有卡牌对。本文用Java实现Memory游戏的核心逻辑,重点讲解Fisher-Yates洗牌算法的均匀随机性保证,以及游戏状态管理与最优翻牌策略的评估。

游戏设计

Memory的核心数据结构包含:
1. 卡牌池:N 对相同图案的卡牌,共 2N 张。
2. 游戏状态:每张卡牌的状态(背面朝上、正面朝上、已消除)。
3. 回合逻辑:玩家每回合最多翻两张牌,匹配则消除,不匹配则翻回。

核心算法:Fisher-Yates洗牌

保证每张排列等概率出现是Memory公平性的关键。Fisher-Yates算法通过从后向前遍历,将当前位置与 [0, i] 范围内的随机位置交换,确保 n! 种排列每种出现的概率严格为 1/n!。

// Fisher-Yates洗牌(Knuth Shuffle)
for (int i = n - 1; i > 0; i--) {
    int j = random.nextInt(i + 1); // [0, i] 均匀随机
    swap(array, i, j);
}

对比常见的错误实现(如 random.nextInt(n) 交换),Fisher-Yates避免了排列不均匀的问题。

Java实现

import java.util.*;

/**
 * Memory记忆匹配游戏:洗牌算法与状态管理
 */
public class MemoryGame {
    private static final int PAIRS = 8; // 8对卡牌
    private Card[] board;
    private Random random;
    private int matchedPairs; // 已匹配对数
    private int moves;        // 总翻牌次数
    private List<Integer> flipped; // 当前翻开的卡牌索引

    /**
     * 卡牌类:封装图案、状态与位置
     */
    static class Card {
        int id;       // 图案编号(相同id为一对)
        int index;    // 在棋盘上的位置
        State state;  // 背面、正面、已消除

        enum State { HIDDEN, REVEALED, REMOVED }

        Card(int id, int index) {
            this.id = id;
            this.index = index;
            this.state = State.HIDDEN;
        }
    }

    public MemoryGame() {
        this.random = new Random();
        initBoard();
    }

    /**
     * 初始化棋盘:生成卡牌对并执行Fisher-Yates洗牌
     */
    private void initBoard() {
        board = new Card[PAIRS * 2];
        // 生成配对卡牌
        for (int i = 0; i < PAIRS; i++) {
            board[i * 2] = new Card(i, i * 2);
            board[i * 2 + 1] = new Card(i, i * 2 + 1);
        }
        // Fisher-Yates洗牌
        for (int i = board.length - 1; i > 0; i--) {
            int j = random.nextInt(i + 1);
            Card temp = board[i];
            board[i] = board[j];
            board[j] = temp;
            // 更新索引
            board[i].index = i;
            board[j].index = j;
        }
        this.flipped = new ArrayList<>();
        this.matchedPairs = 0;
        this.moves = 0;
    }

    /**
     * 翻牌操作:返回操作结果
     * @param index 卡牌位置
     * @return 0=继续, 1=匹配成功, 2=匹配失败, 3=游戏结束
     */
    public int flip(int index) {
        Card card = board[index];
        if (card.state != Card.State.HIDDEN || flipped.size() >= 2) {
            return 0; // 非法操作
        }
        card.state = Card.State.REVEALED;
        flipped.add(index);
        moves++;

        if (flipped.size() == 2) {
            Card first = board[flipped.get(0)];
            Card second = board[flipped.get(1)];
            if (first.id == second.id) {
                // 匹配成功
                first.state = Card.State.REMOVED;
                second.state = Card.State.REMOVED;
                matchedPairs++;
                flipped.clear();
                return (matchedPairs == PAIRS) ? 3 : 1;
            } else {
                return 2; // 匹配失败,需要翻回
            }
        }
        return 0;
    }

    /**
     * 翻回未匹配的卡牌(通常在延迟后调用)
     */
    public void hideUnmatched() {
        for (int idx : flipped) {
            if (board[idx].state == Card.State.REVEALED) {
                board[idx].state = Card.State.HIDDEN;
            }
        }
        flipped.clear();
    }

    /**
     * 获取当前棋盘状态(用于AI决策)
     */
    public Card.State[] getBoardState() {
        Card.State[] states = new Card.State[board.length];
        for (int i = 0; i < board.length; i++) {
            states[i] = board[i].state;
        }
        return states;
    }

    /**
     * AI策略:基于已知信息的最优翻牌
     * 优先级:1) 已知配对 2) 随机探索
     */
    public int[] aiMove() {
        // 收集已知信息
        Map<Integer, List<Integer>> knownIds = new HashMap<>();
        List<Integer> hidden = new ArrayList<>();
        for (int i = 0; i < board.length; i++) {
            if (board[i].state == Card.State.HIDDEN) {
                hidden.add(i);
            } else if (board[i].state == Card.State.REVEALED) {
                knownIds.computeIfAbsent(board[i].id, k -> new ArrayList<>()).add(i);
            }
        }
        // 策略1:若某id已翻开一张,找其配对
        for (Map.Entry<Integer, List<Integer>> entry : knownIds.entrySet()) {
            if (entry.getValue().size() == 1) {
                int targetId = entry.getKey();
                for (int idx : hidden) {
                    if (board[idx].id == targetId) {
                        return new int[]{entry.getValue().get(0), idx};
                    }
                }
            }
        }
        // 策略2:随机翻开两张未探索卡牌
        Collections.shuffle(hidden, random);
        return new int[]{hidden.get(0), hidden.get(1)};
    }

    public boolean isGameOver() {
        return matchedPairs == PAIRS;
    }

    public int getMoves() {
        return moves;
    }

    public void printBoard() {
        for (int i = 0; i < board.length; i++) {
            if (board[i].state == Card.State.REMOVED) {
                System.out.print("[  ] ");
            } else if (board[i].state == Card.State.REVEALED) {
                System.out.printf("[%2d] ", board[i].id);
            } else {
                System.out.print("[??] ");
            }
            if ((i + 1) % 4 == 0) System.out.println();
        }
    }

    public static void main(String[] args) {
        MemoryGame game = new MemoryGame();
        System.out.println("初始棋盘(洗牌后):");
        game.printBoard();
        System.out.println("\nAI自动对局:");
        while (!game.isGameOver()) {
            int[] move = game.aiMove();
            int res1 = game.flip(move[0]);
            int res2 = game.flip(move[1]);
            System.out.printf("翻 %d 和 %d: ", move[0], move[1]);
            if (res1 == 1 || res2 == 1) {
                System.out.println("匹配!");
            } else {
                System.out.println("不匹配");
                game.hideUnmatched();
            }
        }
        System.out.println("游戏结束,总步数: " + game.getMoves());
    }
}

最优策略分析

Memory存在信息完备的最优策略。设当前有 k 张已翻开但未匹配的卡牌(已知信息),AI的期望步数可通过动态规划计算:
– 状态定义:dp[known][unknown] 表示剩余 known 张已知单牌、unknown 对未知牌时的期望翻牌次数。
– 转移方程:优先匹配已知对(若存在),否则计算随机翻牌的期望收益。

对于人类玩家,记忆容量有限,游戏的乐趣在于有限记忆下的概率决策。上述AI策略虽非全局最优,但已能稳定击败随机玩家。

扩展方向

  • ** 多玩家对战**:引入回合制与计分系统,匹配成功可连续翻牌。
  • ** 难度梯度**:增加卡牌数量、加入干扰图案、限制翻牌时间。
  • ** 神经网络记忆模型**:用LSTM模拟人类玩家的有限记忆与遗忘曲线,构建拟人化AI对手。