空当接龙(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 空当接龙的三种核心移动
- Tableau → Foundation:tableau栈顶的牌可以放到同花色的foundation上,条件是点数刚好大1(Ace可以直接放)。
- Tableau → Tableau:可以将tableau栈顶(或连在一起的连续序列)移动到另一列,要求颜色交替且点数递减。移动序列的最大长度受限于空闲单元格数。
- 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与启发式权重,可以在求解速度与解质量之间灵活权衡。