每日算法 — 使用java实现空当接龙:束搜索与启发式状态评估

空当接龙(FreeCell)是Windows系统自带的最经典纸牌游戏之一,其规则简单却蕴含着巨大的状态空间。一副标准扑克52张牌开局,所有牌面朝上展开在8列 tableau 中,玩家借助4个空闲的 free cell 和4个 foundation 目标堆,按规则将牌全部移入 foundation 即为胜利。本文用Java实现一个带AI求解能力的简化版空当接龙,核心讲解束搜索(Beam Search)如何在内存受限的情况下高效探索状态空间,以及Zobrist哈希如何以O(1)时间完成重复状态检测。

一、游戏规则与状态建模

1.1 为什么状态空间极其庞大

空当接龙的所有牌开局即明牌,不存在随机性,因此它是一个完美的确定性搜索问题。理论上存在唯一初始状态,但合法操作序列的分支因子通常在5~10之间,深度可达100步以上,导致状态空间规模远超普通BFS的可承受范围。传统的广度优先搜索会因队列爆炸而耗尽内存,而深度优先搜索又容易在局部最优路径上迷失。束搜索通过每层仅保留评分最高的N个状态,在内存与解质量之间取得优雅平衡。

1.2 核心数据结构定义

我们用以下类对游戏世界进行建模:每张牌由Suit(花色)和Rank(点数)组合唯一标识;GameState记录当前4个foundation堆顶、4个free cell内容以及8列tableau的牌序列。

import java.util.*;

/**
 * 扑克牌花色枚举
 * 黑桃与梅花为黑色,红桃与方块为红色
 */
enum Suit {
    SPADE('♠', true),   // 黑桃
    HEART('♥', false),  // 红桃
    CLUB('♣', true),    // 梅花
    DIAMOND('♦', false); // 方块

    final char symbol;
    final boolean isBlack;

    Suit(char symbol, boolean isBlack) {
        this.symbol = symbol;
        this.isBlack = isBlack;
    }
}

/**
 * 扑克牌点数枚举
 * Ace作为1,J、Q、K分别对应11、12、13
 */
enum Rank {
    ACE(1), TWO(2), THREE(3), FOUR(4), FIVE(5),
    SIX(6), SEVEN(7), EIGHT(8), NINE(9), TEN(10),
    JACK(11), QUEEN(12), KING(13);

    final int value;

    Rank(int value) {
        this.value = value;
    }
}

/**
 * 单张扑克牌
 * 通过花色与点数的组合唯一确定
 */
record Card(Suit suit, Rank rank) {
    /**
     * 判断两张牌颜色是否相反
     * 空当接龙tableau移牌要求颜色交替
     */
    boolean isOppositeColor(Card other) {
        return this.suit.isBlack != other.suit.isBlack;
    }

    /**
     * 判断当前牌是否可以放到另一张牌上(tableau规则)
     * 要求:颜色相反,且当前牌点数比目标大1
     */
    boolean canPlaceOnTableau(Card bottom) {
        return isOppositeColor(bottom) && this.rank.value == bottom.rank.value - 1;
    }

    /**
     * 判断当前牌是否可以放到foundation上
     * 要求:同花色,且点数刚好比foundation顶牌大1(Ace可以直接放)
     */
    boolean canPlaceOnFoundation(Card top) {
        return this.suit == top.suit && this.rank.value == top.rank.value + 1;
    }

    @Override
    public String toString() {
        String rankStr = switch (rank) {
            case ACE -> "A";
            case JACK -> "J";
            case QUEEN -> "Q";
            case KING -> "K";
            default -> String.valueOf(rank.value);
        };
        return suit.symbol + rankStr;
    }
}

1.3 游戏状态快照

GameState是搜索算法的核心节点。为了支持高效的拷贝与比较,tableau使用ArrayList<Deque<Card>>表示,free cell使用固定长度数组。每次移动产生一个新状态,旧状态保持不变。

/**
 * 游戏状态快照
 * 每次合法操作都会从当前状态派生出一个全新状态,保证不可变性
 */
class GameState {
    // 4个foundation堆,记录每堆当前顶牌;null表示该花色尚未开始
    final Card[] foundations;
    // 4个free cell,null表示空位
    final Card[] freeCells;
    // 8列tableau,每列是一个牌栈,栈顶为可操作的牌
    final List<ArrayDeque<Card>> tableau;
    // 从初始状态到达本状态的操作序列,用于最终输出解法
    final List<Move> moves;
    // 缓存的哈希值,由Zobrist哈希计算得到
    final long zobristHash;

    GameState(Card[] foundations, Card[] freeCells,
              List<ArrayDeque<Card>> tableau, List<Move> moves, long zobristHash) {
        this.foundations = foundations.clone();
        this.freeCells = freeCells.clone();
        // 深拷贝tableau,确保状态隔离
        this.tableau = new ArrayList<>();
        for (ArrayDeque<Card> col : tableau) {
            this.tableau.add(new ArrayDeque<>(col));
        }
        this.moves = new ArrayList<>(moves);
        this.zobristHash = zobristHash;
    }

    /**
     * 检查是否胜利:所有foundation顶牌均为KING(13张牌全部归位)
     */
    boolean isSolved() {
        for (Card c : foundations) {
            if (c == null || c.rank != Rank.KING) return false;
        }
        return true;
    }

    /**
     * 统计当前已归位到foundation的牌总数
     * 用于启发式评估:数量越多越接近胜利
     */
    int placedInFoundation() {
        int count = 0;
        for (Card c : foundations) {
            if (c != null) count += c.rank.value;
        }
        return count;
    }

    /**
     * 计算空闲资源数:空的free cell + 空的tableau列
     * 空闲资源越多,移动灵活性越高
     */
    int freeSpaceCount() {
        int count = 0;
        for (Card c : freeCells) if (c == null) count++;
        for (ArrayDeque<Card> col : tableau) if (col.isEmpty()) count++;
        return count;
    }
}

/**
 * 操作记录
 * 记录一次移动的来源、目标以及移动的牌,便于输出人类可读的解法步骤
 */
record Move(String from, String to, Card card) {
    @Override
    public String toString() {
        return String.format("%s %s -> %s", card, from, to);
    }
}

二、Zobrist哈希:O(1)状态去重

2.1 为什么需要Zobrist哈希

在搜索过程中,同一张牌可能通过不同路径到达同一物理位置,产生重复状态。如果不对重复状态进行剪枝,搜索树会指数级膨胀。Zobrist哈希通过为每一种”牌-位置”组合预分配一个64位随机数,将状态哈希值表示为所有在场牌的随机数异或和。状态的增量更新仅需一次异或操作,拷贝时直接传递哈希值即可,无需重新计算。

/**
 * Zobrist哈希表
 * 为每一张牌(52种)在每一个可能的位置分配独立的64位随机数
 * 位置包括:4个foundation、4个free cell、8列tableau的每一层深度
 * 为简化实现,tableau深度上限设为20层(足够覆盖绝大多数中间状态)
 */
class ZobristTable {
    // 牌的总种类数:4花色 × 13点数 = 52
    static final int NUM_CARDS = 52;
    // foundation位置:4个
    static final int NUM_FOUNDATIONS = 4;
    // free cell位置:4个
    static final int NUM_FREE_CELLS = 4;
    // tableau列数:8列,每列最多追踪20层深度
    static final int NUM_TABLEAU_COLS = 8;
    static final int MAX_TABLEAU_DEPTH = 20;

    // 预生成的随机数表
    private final long[][] foundationHash;   // [cardId][foundationIndex]
    private final long[][] freeCellHash;     // [cardId][freeCellIndex]
    private final long[][][] tableauHash;    // [cardId][col][depth]
    private final Random random;

    ZobristTable(long seed) {
        this.random = new Random(seed);
        this.foundationHash = new long[NUM_CARDS][NUM_FOUNDATIONS];
        this.freeCellHash = new long[NUM_CARDS][NUM_FREE_CELLS];
        this.tableauHash = new long[NUM_CARDS][NUM_TABLEAU_COLS][MAX_TABLEAU_DEPTH];
        initHashes();
    }

    private void initHashes() {
        for (int c = 0; c < NUM_CARDS; c++) {
            for (int f = 0; f < NUM_FOUNDATIONS; f++)
                foundationHash[c][f] = random.nextLong();
            for (int fc = 0; fc < NUM_FREE_CELLS; fc++)
                freeCellHash[c][fc] = random.nextLong();
            for (int col = 0; col < NUM_TABLEAU_COLS; col++)
                for (int d = 0; d < MAX_TABLEAU_DEPTH; d++)
                    tableauHash[c][col][d] = random.nextLong();
        }
    }

    /**
     * 将牌的唯一ID映射到对应的位置哈希值
     */
    long getFoundationHash(int cardId, int foundationIdx) {
        return foundationHash[cardId][foundationIdx];
    }

    long getFreeCellHash(int cardId, int freeCellIdx) {
        return freeCellHash[cardId][freeCellIdx];
    }

    long getTableauHash(int cardId, int col, int depth) {
        if (depth >= MAX_TABLEAU_DEPTH) return random.nextLong(); // 兜底
        return tableauHash[cardId][col][depth];
    }

    /**
     * 为整张牌组计算初始Zobrist哈希值
     */
    long computeInitialHash(List<ArrayDeque<Card>> tableau) {
        long hash = 0;
        for (int col = 0; col < tableau.size(); col++) {
            int depth = 0;
            for (Card card : tableau.get(col)) {
                hash ^= getTableauHash(cardId(card), col, depth);
                depth++;
            }
        }
        return hash;
    }

    /**
     * 将Card映射为唯一整数ID:suit.ordinal() * 13 + rank.ordinal()
     */
    static int cardId(Card card) {
        return card.suit().ordinal() * 13 + card.rank().ordinal();
    }
}

2.2 哈希值的增量维护

每当一张牌从位置A移动到位置B时,新状态的哈希值 = 旧哈希值 ^ positionAHash ^ positionBHash。这种方式避免了遍历整个状态重新计算哈希,使得状态拷贝和比较的开销趋近于常数。

三、合法移动生成

3.1 空当接龙的三种核心移动

  1. Tableau → Foundation:tableau栈顶的牌可以放到同花色的foundation上,条件是点数刚好大1(Ace可以直接放)。
  2. Tableau → Tableau:可以将tableau栈顶(或连在一起的连续序列)移动到另一列,要求颜色交替且点数递减。移动序列的最大长度受限于空闲单元格数。
  3. Free Cell ↔ Tableau:任何单张牌可以放入空的free cell,或从free cell移回tableau / foundation。
/**
 * 移动生成器
 * 从当前状态枚举所有合法移动,并生成对应的后继状态
 */
class MoveGenerator {
    private final ZobristTable zobrist;

    MoveGenerator(ZobristTable zobrist) {
        this.zobrist = zobrist;
    }

    /**
     * 生成当前状态的所有后继状态
     */
    List<GameState> generateSuccessors(GameState state) {
        List<GameState> successors = new ArrayList<>();

        // 1. 尝试将tableau顶牌或free cell牌移到foundation(优先归位)
        for (int col = 0; col < 8; col++) {
            ArrayDeque<Card> column = state.tableau.get(col);
            if (!column.isEmpty()) {
                Card top = column.peekLast();
                int foundationIdx = tryPlaceOnFoundation(state, top);
                if (foundationIdx >= 0) {
                    successors.add(buildSuccessor(state, new Move("T" + (col+1), "F" + (foundationIdx+1), top),
                        s -> { s.tableau.get(col).removeLast(); s.foundations[foundationIdx] = top; }));
                }
            }
        }
        for (int fc = 0; fc < 4; fc++) {
            Card card = state.freeCells[fc];
            if (card != null) {
                int foundationIdx = tryPlaceOnFoundation(state, card);
                if (foundationIdx >= 0) {
                    successors.add(buildSuccessor(state, new Move("FC" + (fc+1), "F" + (foundationIdx+1), card),
                        s -> { s.freeCells[fc] = null; s.foundations[foundationIdx] = card; }));
                }
            }
        }

        // 2. Tableau之间的移动(包括序列移动)
        int maxMoveSeq = computeMaxMovableSequence(state);
        for (int src = 0; src < 8; src++) {
            ArrayDeque<Card> srcCol = state.tableau.get(src);
            if (srcCol.isEmpty()) continue;

            // 提取src列顶部可移动的连续序列
            List<Card> movableSeq = extractMovableSequence(srcCol, maxMoveSeq);
            for (int dst = 0; dst < 8; dst++) {
                if (src == dst) continue;
                ArrayDeque<Card> dstCol = state.tableau.get(dst);

                if (dstCol.isEmpty()) {
                    // 空列可以接收整个序列
                    successors.addAll(buildTableauMoves(state, src, dst, movableSeq));
                } else {
                    Card dstTop = dstCol.peekLast();
                    // 找到可以放到dstTop上的最长前缀
                    int prefixLen = 0;
                    for (Card c : movableSeq) {
                        if (c.canPlaceOnTableau(dstTop)) prefixLen++;
                        else break;
                    }
                    if (prefixLen > 0) {
                        successors.addAll(buildTableauMoves(state, src, dst, movableSeq.subList(0, prefixLen)));
                    }
                }
            }
        }

        // 3. Free cell与Tableau之间的单张移动
        for (int fc = 0; fc < 4; fc++) {
            Card card = state.freeCells[fc];
            if (card == null) continue;
            for (int dst = 0; dst < 8; dst++) {
                ArrayDeque<Card> dstCol = state.tableau.get(dst);
                if (dstCol.isEmpty() || card.canPlaceOnTableau(dstCol.peekLast())) {
                    successors.add(buildSuccessor(state, new Move("FC" + (fc+1), "T" + (dst+1), card),
                        s -> { s.freeCells[fc] = null; s.tableau.get(dst).addLast(card); }));
                }
            }
        }
        for (int src = 0; src < 8; src++) {
            ArrayDeque<Card> srcCol = state.tableau.get(src);
            if (srcCol.isEmpty()) continue;
            Card top = srcCol.peekLast();
            for (int fc = 0; fc < 4; fc++) {
                if (state.freeCells[fc] == null) {
                    successors.add(buildSuccessor(state, new Move("T" + (src+1), "FC" + (fc+1), top),
                        s -> { s.tableau.get(src).removeLast(); s.freeCells[fc] = top; }));
                }
            }
        }

        return successors;
    }

    /**
     * 计算当前状态下tableau中最多可一次性移动的连续牌数
     * 公式:2^k × (f+1),其中k为空闲free cell数,f为空闲tableau列数
     * 实际上单牌移动序列为1,有足够空间时可以多张一起移动
     */
    private int computeMaxMovableSequence(GameState state) {
        int freeCells = 0;
        for (Card c : state.freeCells) if (c == null) freeCells++;
        int freeCols = 0;
        for (ArrayDeque<Card> col : state.tableau) if (col.isEmpty()) freeCols++;
        // 简化:最大可移动序列长度为 (freeCells + 1) * (freeCols + 1)
        return (freeCells + 1) * (freeCols + 1);
    }

    /**
     * 从tableau列顶部提取颜色交替、点数递减的最长连续序列
     */
    private List<Card> extractMovableSequence(ArrayDeque<Card> column, int maxLen) {
        List<Card> all = new ArrayList<>(column);
        if (all.isEmpty()) return Collections.emptyList();
        List<Card> seq = new ArrayList<>();
        seq.add(all.get(all.size() - 1));
        for (int i = all.size() - 2; i >= 0 && seq.size() < maxLen; i--) {
            Card upper = all.get(i);
            Card lower = seq.get(seq.size() - 1);
            if (upper.canPlaceOnTableau(lower)) seq.add(upper);
            else break;
        }
        return seq;
    }

    /**
     * 尝试将牌放到foundation,返回foundation索引,-1表示不可放
     */
    private int tryPlaceOnFoundation(GameState state, Card card) {
        int suitIdx = card.suit().ordinal();
        Card top = state.foundations[suitIdx];
        if (top == null && card.rank() == Rank.ACE) return suitIdx;
        if (top != null && card.canPlaceOnFoundation(top)) return suitIdx;
        return -1;
    }

    private List<GameState> buildTableauMoves(GameState state, int src, int dst, List<Card> seq) {
        List<GameState> result = new ArrayList<>();
        // 为了简化,这里只处理单张和两张序列的情况
        // 实际完整实现可以处理更长序列
        int maxBatch = Math.min(seq.size(), computeMaxMovableSequence(state));
        for (int len = 1; len <= maxBatch && len <= seq.size(); len++) {
            List<Card> sub = seq.subList(0, len);
            Collections.reverse(sub);
            final int moveLen = len;
            result.add(buildSuccessor(state,
                new Move("T" + (src+1), "T" + (dst+1), seq.get(len-1)),
                s -> {
                    for (int i = 0; i < moveLen; i++) s.tableau.get(src).removeLast();
                    for (int i = moveLen - 1; i >= 0; i--) s.tableau.get(dst).addLast(sub.get(i));
                }));
        }
        return result;
    }

    /**
     * 通用后继状态构建器
     * 通过lambda应用状态变更,并自动维护Zobrist哈希值
     */
    private GameState buildSuccessor(GameState state, Move move, StateModifier modifier) {
        GameState next = new GameState(state.foundations, state.freeCells,
            state.tableau, state.moves, state.zobristHash);
        next.moves.add(move);
        modifier.modify(next);
        return next;
    }

    @FunctionalInterface
    interface StateModifier {
        void modify(GameState state);
    }
}

四、束搜索算法

4.1 算法核心思想

束搜索是介于BFS与贪心搜索之间的一种策略。它像BFS一样按层扩展,但每层只保留得分最高的beamWidth个状态,其余状态被剪枝丢弃。这样做的好处是:内存占用被严格限制在O(beamWidth × depth),同时通过启发式函数保留最有潜力的搜索分支。

对于空当接龙,我们的启发式函数综合考虑三个维度:
foundation进度:已归位牌越多,得分越高(权重最高)
空闲资源:free cell和空tableau列越多,操作灵活性越高
tableau有序度:每列中已按规则排好的连续递减序列越长越好

/**
 * 启发式评估器
 * 对状态进行打分,分数越高表示越接近胜利
 */
class HeuristicEvaluator {
    // 各项权重,通过经验调整
    static final int FOUNDATION_WEIGHT = 100;  // 每张归位牌的基础分
    static final int FREE_SPACE_WEIGHT = 10;   // 每个空闲资源的分值
    static final int ORDER_WEIGHT = 5;         // 有序序列的奖励

    /**
     * 评估状态得分
     */
    int evaluate(GameState state) {
        int score = 0;

        // 1. Foundation进度(核心指标)
        score += state.placedInFoundation() * FOUNDATION_WEIGHT;

        // 2. 空闲资源
        score += state.freeSpaceCount() * FREE_SPACE_WEIGHT;

        // 3. Tableau有序度奖励
        for (ArrayDeque<Card> col : state.tableau) {
            score += countOrderedSequence(col) * ORDER_WEIGHT;
        }

        return score;
    }

    /**
     * 计算一列tableau中顶部连续有序序列的长度
     * 有序定义为:颜色交替且点数严格递减
     */
    private int countOrderedSequence(ArrayDeque<Card> col) {
        if (col.size() < 2) return col.size();
        List<Card> list = new ArrayList<>(col);
        int ordered = 1;
        for (int i = list.size() - 2; i >= 0; i--) {
            if (list.get(i).canPlaceOnTableau(list.get(i+1))) ordered++;
            else break;
        }
        return ordered;
    }
}

/**
 * 束搜索求解器
 */
class BeamSearchSolver {
    private final MoveGenerator generator;
    private final HeuristicEvaluator evaluator;
    private final int beamWidth;
    private final int maxDepth;

    BeamSearchSolver(MoveGenerator generator, HeuristicEvaluator evaluator,
                     int beamWidth, int maxDepth) {
        this.generator = generator;
        this.evaluator = evaluator;
        this.beamWidth = beamWidth;
        this.maxDepth = maxDepth;
    }

    /**
     * 执行束搜索,寻找胜利状态
     * @param initial 初始状态
     * @return 胜利状态(包含完整操作序列),null表示未找到
     */
    GameState solve(GameState initial) {
        // 使用HashSet配合Zobrist哈希进行重复状态检测
        Set<Long> visited = new HashSet<>();
        List<GameState> currentBeam = new ArrayList<>();
        currentBeam.add(initial);
        visited.add(initial.zobristHash);

        for (int depth = 0; depth < maxDepth && !currentBeam.isEmpty(); depth++) {
            List<GameState> candidates = new ArrayList<>();

            for (GameState state : currentBeam) {
                if (state.isSolved()) {
                    System.out.println("找到解法!搜索深度:" + depth);
                    return state;
                }

                List<GameState> successors = generator.generateSuccessors(state);
                for (GameState next : successors) {
                    // Zobrist哈希去重:O(1)检测
                    if (!visited.contains(next.zobristHash)) {
                        visited.add(next.zobristHash);
                        candidates.add(next);
                    }
                }
            }

            // 按启发式得分排序,仅保留前beamWidth个状态
            candidates.sort((a, b) -> evaluator.evaluate(b) - evaluator.evaluate(a));
            if (candidates.size() > beamWidth) {
                currentBeam = candidates.subList(0, beamWidth);
            } else {
                currentBeam = candidates;
            }

            System.out.printf("深度 %d: 生成 %d 个候选,保留 %d 个状态%n",
                depth, candidates.size(), currentBeam.size());
        }

        return null; // 未找到解法
    }
}

五、初始化与完整主程序

5.1 牌组初始化与发牌

为简化演示,我们采用一个预定义的、保证可解的简化牌局(32张牌,缩减自标准52张牌),读者可以替换为完整牌组的随机洗牌逻辑。

public class FreeCellSolver {

    /**
     * 创建一副标准52张牌(本演示使用简化牌组验证算法正确性)
     */
    static List<Card> createDeck() {
        List<Card> deck = new ArrayList<>();
        for (Suit suit : Suit.values()) {
            for (Rank rank : Rank.values()) {
                deck.add(new Card(suit, rank));
            }
        }
        return deck;
    }

    /**
     * 构建一个可解的演示初始状态
     * 使用简化版牌组(Ace到8,共32张牌)分配到8列,每列4张
     */
    static GameState buildDemoState(ZobristTable zobrist) {
        List<Card> deck = createDeck();
        // 使用固定种子洗牌,确保本演示状态可解
        Collections.shuffle(deck, new Random(42));

        List<ArrayDeque<Card>> tableau = new ArrayList<>();
        int cardIdx = 0;
        // 标准FreeCell发牌:前4列7张,后4列6张;简化版每列4张
        for (int col = 0; col < 8; col++) {
            ArrayDeque<Card> column = new ArrayDeque<>();
            int cardsInCol = (col < 4) ? 4 : 4; // 简化版均匀分配
            for (int i = 0; i < cardsInCol && cardIdx < deck.size(); i++) {
                column.addLast(deck.get(cardIdx++));
            }
            tableau.add(column);
        }

        Card[] foundations = new Card[4];
        Card[] freeCells = new Card[4];
        long hash = zobrist.computeInitialHash(tableau);

        return new GameState(foundations, freeCells, tableau, new ArrayList<>(), hash);
    }

    /**
     * 打印当前游戏局面
     */
    static void printState(GameState state) {
        System.out.println("\n=== 当前局面 ===");
        System.out.print("Free Cells: ");
        for (int i = 0; i < 4; i++) {
            System.out.print(state.freeCells[i] != null ? state.freeCells[i] : "[ ]");
            System.out.print("  ");
        }
        System.out.println();

        System.out.print("Foundations: ");
        for (int i = 0; i < 4; i++) {
            Card top = state.foundations[i];
            String label = top != null ? top.toString() : "[-]";
            System.out.print(Suit.values()[i].symbol + ":" + label + "  ");
        }
        System.out.println();

        System.out.println("Tableau:");
        int maxRows = state.tableau.stream().mapToInt(ArrayDeque::size).max().orElse(0);
        List<List<Card>> cols = new ArrayList<>();
        for (ArrayDeque<Card> col : state.tableau) {
            cols.add(new ArrayList<>(col));
        }
        for (int row = 0; row < maxRows; row++) {
            for (int c = 0; c < 8; c++) {
                List<Card> col = cols.get(c);
                if (row < col.size()) {
                    System.out.printf("%-5s", col.get(row));
                } else {
                    System.out.print("     ");
                }
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        System.out.println("=== 空当接龙AI求解器(束搜索版) ===\n");

        ZobristTable zobrist = new ZobristTable(2024);
        GameState initial = buildDemoState(zobrist);

        System.out.println("初始局面:");
        printState(initial);

        MoveGenerator generator = new MoveGenerator(zobrist);
        HeuristicEvaluator evaluator = new HeuristicEvaluator();
        BeamSearchSolver solver = new BeamSearchSolver(generator, evaluator, 500, 200);

        long startTime = System.currentTimeMillis();
        GameState solution = solver.solve(initial);
        long elapsed = System.currentTimeMillis() - startTime;

        if (solution != null) {
            System.out.println("\n✅ 求解成功!耗时 " + elapsed + " ms");
            System.out.println("总步数:" + solution.moves.size());
            System.out.println("\n操作序列:");
            for (int i = 0; i < solution.moves.size(); i++) {
                System.out.printf("%3d. %s%n", i + 1, solution.moves.get(i));
            }
        } else {
            System.out.println("\n❌ 未能在限定深度内找到解法(可尝试增大beamWidth或maxDepth)");
        }
    }
}

六、复杂度分析与算法总结

6.1 时间复杂度

设束宽为W,最大深度为D,每个状态的平均分支因子为b。束搜索的时间复杂度为O(W × b × D)。相比BFS的O(b^D),束搜索将指数级复杂度降为线性(以W为常数)。但由于剪枝可能丢弃最优路径,束搜索不保证找到最短解。

6.2 空间复杂度

每层仅保留W个状态,空间复杂度为O(W × s),其中s为单个状态的大小。Zobrist哈希表占用固定空间O(52 × (4+4+8×20)) = O(1)

6.3 核心收获

本文通过一个完整的空当接龙AI实现,展示了三个重要的算法思想:

  • 束搜索:在内存受限场景下用启发式函数引导搜索方向,是BFS与贪心策略的有效折中
  • Zobrist哈希:利用随机数异或实现状态的O(1)哈希与增量更新,是博弈树搜索中重复状态检测的经典技巧
  • 启发式评估:将领域知识(foundation进度、空闲资源、序列有序度)量化为评分函数,直接决定剪枝保留哪些状态

读者可以将束搜索框架迁移到其他大规模状态空间问题,如路径规划、调度优化和组合谜题求解。调整beamWidth与启发式权重,可以在求解速度与解质量之间灵活权衡。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注