每日算法 — 使用java实现自由细胞:回溯搜索与空位调度策略

引言

自由细胞(FreeCell)是Windows系统自带的一款经典纸牌接龙游戏。与多数纸牌游戏不同,FreeCell的几乎所有牌局都是可解的(约99.999%),这使得它成为研究搜索算法的理想对象。本文用Java实现FreeCell的核心求解器,采用深度优先回溯搜索配合空位调度启发式,演示如何系统地探索巨大的纸牌状态空间。

游戏规则与状态建模

FreeCell使用一副标准52张扑克牌,布局如下:
– 8个级联列(Cascades):初始时每列发牌,前4列7张、后4列6张;
– 4个自由细胞(FreeCells):临时存放单张牌的空位;
– 4个目标基座(Foundations):按花色从A到K依次构建的收牌区。

移动规则:
1. 级联列中的牌可按红黑交替、点数递减的规则移动;
2. 自由细胞每次只能放一张牌;
3. 目标基座按同花色、点数递增收牌;
4. 一次可移动的牌组数量受空自由细胞与空级列数限制。

可移动牌组数量的计算

这是FreeCell搜索的核心约束。假设有 f 个空自由细胞、e 个空级列,则一次可移动的连续递减牌组最大长度为:

maxMove = (f + 1) × 2^e

该公式源于:每多一个空自由细胞可多带1张牌;每多一个空级列,可借助其作为中转站实现指数级倍增。

Java实现

import java.util.*;

/**
 * FreeCell 求解器:回溯搜索 + 空位调度启发式
 */
public class FreeCellSolver {

    // 牌面编码:0~51,花色=suit/13,点数=rank%13(0=A,12=K)
    static final int SUITS = 4;
    static final int RANKS = 13;
    static final int TOTAL_CARDS = 52;

    /**
     * 游戏状态:级联列 + 自由细胞 + 目标基座
     */
    static class State {
        List<Deque<Integer>> cascades; // 8个级联列,每列是一个栈
        Integer[] freeCells;             // 4个自由细胞,null表示空
        int[] foundations;               // 4个目标基座当前最高点数(-1表示空)
        int moveCount;                   // 已走步数
        State parent;                    // 前驱状态(用于回溯)
        String moveDesc;                 // 移动描述

        State() {
            cascades = new ArrayList<>();
            for (int i = 0; i < 8; i++) cascades.add(new ArrayDeque<>());
            freeCells = new Integer[4];
            foundations = new int[]{-1, -1, -1, -1};
            moveCount = 0;
        }

        @Override
        public int hashCode() {
            int h = Arrays.hashCode(freeCells);
            h = h * 31 + Arrays.hashCode(foundations);
            for (Deque<Integer> col : cascades) {
                h = h * 31 + col.hashCode();
            }
            return h;
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof State)) return false;
            State other = (State) o;
            return Arrays.equals(freeCells, other.freeCells)
                && Arrays.equals(foundations, other.foundations)
                && cascadesEquals(other.cascades);
        }

        boolean cascadesEquals(List<Deque<Integer>> other) {
            if (cascades.size() != other.size()) return false;
            for (int i = 0; i < cascades.size(); i++) {
                if (!cascades.get(i).equals(other.get(i))) return false;
            }
            return true;
        }

        State copy() {
            State s = new State();
            for (int i = 0; i < 8; i++) {
                s.cascades.set(i, new ArrayDeque<>(cascades.get(i)));
            }
            s.freeCells = freeCells.clone();
            s.foundations = foundations.clone();
            s.moveCount = moveCount;
            return s;
        }
    }

    static int suit(int card) { return card / RANKS; }
    static int rank(int card) { return card % RANKS; }
    static boolean isRed(int card) { return suit(card) < 2; }

    static boolean canPlaceOnCascade(int top, int card) {
        return isRed(top) != isRed(card) && rank(top) == rank(card) + 1;
    }

    static int maxMovableSequence(State s) {
        int freeCount = 0;
        for (Integer c : s.freeCells) if (c == null) freeCount++;
        int emptyCascade = 0;
        for (Deque<Integer> col : s.cascades) if (col.isEmpty()) emptyCascade++;
        return (freeCount + 1) * (1 << emptyCascade);
    }

    static List<Integer> getMovableSequence(Deque<Integer> col) {
        List<Integer> seq = new ArrayList<>();
        if (col.isEmpty()) return seq;
        Iterator<Integer> it = col.descendingIterator();
        int prev = -1;
        while (it.hasNext()) {
            int card = it.next();
            if (prev == -1 || canPlaceOnCascade(prev, card)) {
                seq.add(card);
                prev = card;
            } else {
                break;
            }
        }
        return seq;
    }

    static List<State> generateMoves(State s) {
        List<State> moves = new ArrayList<>();
        int maxMove = maxMovableSequence(s);

        // 1. 尝试将牌直接移到目标基座(优先操作,加速收敛)
        for (int i = 0; i < 4; i++) {
            if (s.freeCells[i] != null) {
                int card = s.freeCells[i];
                if (canMoveToFoundation(s, card)) {
                    State ns = s.copy();
                    ns.freeCells[i] = null;
                    ns.foundations[suit(card)] = rank(card);
                    ns.moveCount = s.moveCount + 1;
                    ns.parent = s;
                    ns.moveDesc = "FreeCell " + cardName(card) + " -> Foundation";
                    moves.add(ns);
                }
            }
        }
        for (int c = 0; c < 8; c++) {
            Deque<Integer> col = s.cascades.get(c);
            if (!col.isEmpty()) {
                int card = col.peekLast();
                if (canMoveToFoundation(s, card)) {
                    State ns = s.copy();
                    ns.cascades.get(c).pollLast();
                    ns.foundations[suit(card)] = rank(card);
                    ns.moveCount = s.moveCount + 1;
                    ns.parent = s;
                    ns.moveDesc = "Cascade " + c + " " + cardName(card) + " -> Foundation";
                    moves.add(ns);
                }
            }
        }

        // 2. 级联列之间的移动
        for (int src = 0; src < 8; src++) {
            List<Integer> seq = getMovableSequence(s.cascades.get(src));
            int maxLen = Math.min(seq.size(), maxMove);
            for (int len = 1; len <= maxLen; len++) {
                int topCard = seq.get(len - 1);
                for (int dst = 0; dst < 8; dst++) {
                    if (src == dst) continue;
                    Deque<Integer> dstCol = s.cascades.get(dst);
                    if (dstCol.isEmpty() || canPlaceOnCascade(dstCol.peekLast(), topCard)) {
                        State ns = s.copy();
                        Deque<Integer> srcCol = ns.cascades.get(src);
                        List<Integer> moved = new ArrayList<>();
                        for (int k = 0; k < len; k++) moved.add(srcCol.pollLast());
                        Collections.reverse(moved);
                        for (int card : moved) ns.cascades.get(dst).addLast(card);
                        ns.moveCount = s.moveCount + 1;
                        ns.parent = s;
                        ns.moveDesc = "Cascade " + src + " -> " + dst + " (" + len + " cards)";
                        moves.add(ns);
                    }
                }
            }
        }

        // 3. 级联列 -> 自由细胞
        for (int c = 0; c < 8; c++) {
            if (s.cascades.get(c).isEmpty()) continue;
            for (int f = 0; f < 4; f++) {
                if (s.freeCells[f] == null) {
                    State ns = s.copy();
                    int card = ns.cascades.get(c).pollLast();
                    ns.freeCells[f] = card;
                    ns.moveCount = s.moveCount + 1;
                    ns.parent = s;
                    ns.moveDesc = "Cascade " + c + " " + cardName(card) + " -> FreeCell";
                    moves.add(ns);
                    break;
                }
            }
        }

        // 4. 自由细胞 -> 级联列
        for (int f = 0; f < 4; f++) {
            if (s.freeCells[f] == null) continue;
            int card = s.freeCells[f];
            for (int c = 0; c < 8; c++) {
                Deque<Integer> col = s.cascades.get(c);
                if (col.isEmpty() || canPlaceOnCascade(col.peekLast(), card)) {
                    State ns = s.copy();
                    ns.freeCells[f] = null;
                    ns.cascades.get(c).addLast(card);
                    ns.moveCount = s.moveCount + 1;
                    ns.parent = s;
                    ns.moveDesc = "FreeCell " + cardName(card) + " -> Cascade " + c;
                    moves.add(ns);
                }
            }
        }

        return moves;
    }

    static boolean canMoveToFoundation(State s, int card) {
        return rank(card) == s.foundations[suit(card)] + 1;
    }

    static String cardName(int card) {
        String[] suits = {"♠", "♥", "♦", "♣"};
        String[] ranks = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
        return ranks[rank(card)] + suits[suit(card)];
    }

    static boolean isSolved(State s) {
        for (int f : s.foundations) {
            if (f != RANKS - 1) return false;
        }
        return true;
    }

    static int heuristic(State s) {
        int sum = 0;
        for (int f : s.foundations) sum += f + 1;
        int freeCount = 0;
        for (Integer c : s.freeCells) if (c == null) freeCount++;
        return sum * 10 + freeCount;
    }

    static State solve(State initial, int maxDepth) {
        Set<Integer> visited = new HashSet<>();
        return dfs(initial, 0, maxDepth, visited);
    }

    static State dfs(State s, int depth, int maxDepth, Set<Integer> visited) {
        if (isSolved(s)) return s;
        if (depth >= maxDepth) return null;

        int hash = s.hashCode();
        if (visited.contains(hash)) return null;
        visited.add(hash);

        List<State> moves = generateMoves(s);
        moves.sort((a, b) -> heuristic(b) - heuristic(a));

        for (State next : moves) {
            State result = dfs(next, depth + 1, maxDepth, visited);
            if (result != null) return result;
        }

        visited.remove(hash);
        return null;
    }

    static State iddfsSolve(State initial) {
        for (int depth = 1; depth <= 200; depth++) {
            System.out.println("尝试深度限制:" + depth);
            Set<Integer> visited = new HashSet<>();
            State result = dfs(initial, 0, depth, visited);
            if (result != null) return result;
        }
        return null;
    }

    static void printSolution(State goal) {
        List<String> path = new ArrayList<>();
        State cur = goal;
        while (cur.parent != null) {
            path.add(cur.moveDesc);
            cur = cur.parent;
        }
        Collections.reverse(path);
        System.out.println("\n解法步数:" + path.size());
        for (int i = 0; i < path.size(); i++) {
            System.out.println((i + 1) + ". " + path.get(i));
        }
    }

    static State createSimpleState() {
        State s = new State();
        int card = 0;
        for (int c = 0; c < 4; c++) {
            for (int r = 0; r < 3; r++) {
                s.cascades.get(c).addLast(card++);
            }
        }
        return s;
    }

    public static void main(String[] args) {
        State initial = createSimpleState();
        System.out.println("开始求解简化版 FreeCell...");
        State goal = iddfsSolve(initial);
        if (goal != null) {
            System.out.println("求解成功!");
            printSolution(goal);
        } else {
            System.out.println("未找到解法(在搜索深度限制内)。");
        }
    }
}

复杂度分析

  • 状态空间:52张牌的排列极为庞大,但实际可达状态受规则约束。
  • 分支因子:平均每步约20~40种合法移动。
  • 时间复杂度:O(b^d),其中 b 为分支因子,d 为解深度。迭代加深将空间控制在 O(bd)。
  • 优化手段:优先将可收牌移至基座、启发式排序减少无效搜索、哈希判重避免循环状态。

总结

FreeCell求解器展示了回溯搜索在组合博弈中的应用。核心难点在于状态表示的精确性、可移动牌组数量的正确计算,以及搜索策略的剪枝效率。本文的框架可进一步扩展为完整52张牌求解器,引入更强的启发式(如优先清空列、避免将低牌压在高牌下)和双向搜索,以应对真实牌局的挑战。