每日算法 — 使用java实现华容道:遗传算法与染色体编码进化求解

引言

华容道是中国传统益智游戏,玩家需要在有限的空间中移动方块,将目标棋子(曹操)移至出口。其状态空间极其庞大,传统BFS或A*搜索在复杂局面下容易因状态爆炸而效率低下。本文将遗传算法(Genetic Algorithm)引入华容道求解,通过染色体编码、交叉变异与适应度选择,让种群在进化中自动发现最优解路径。

算法核心思想

遗传算法模拟生物进化过程:

  • 染色体编码:将华容道的每一步移动编码为基因序列
  • 初始种群:随机生成多组可行移动序列
  • 适应度评估:根据曹操到出口的距离、移动步数、死锁惩罚综合评分
  • 选择:优胜劣汰,保留高适应度个体
  • 交叉:两条染色体交换片段,组合优良移动模式
  • 变异:随机改变某些基因(移动方向),维持种群多样性
  • 精英保留:每代最优个体直接进入下一代,防止优秀基因丢失

核心数据结构

import java.util.*;

/**
 * 华容道遗传算法求解器
 * 采用经典布局:4×5网格,曹操为2×2,五虎将各1×2或2×1,四个1×1小兵
 */
public class GeneticAlgorithmKlondike {

    // 棋盘尺寸
    private static final int ROWS = 5;
    private static final int COLS = 4;

    // 棋子类型
    private static final int EMPTY = 0;
    private static final int CAOCAO = 1;   // 曹操 2×2
    private static final int GENERAL = 2;  // 将领 1×2 或 2×1
    private static final int SOLDIER = 3;  // 小兵 1×1

    // 移动方向编码:0=上,1=下,2=左,3=右
    private static final int[][] DIRS = {{-1,0},{1,0},{0,-1},{0,1}};
    private static final String[] DIR_NAMES = {"上","下","左","右"};

    private final Random random;

    // 遗传算法参数
    private final int populationSize;  // 种群大小
    private final int maxGenerations;  // 最大迭代代数
    private final double crossoverRate; // 交叉概率
    private final double mutationRate;  // 变异概率
    private final int chromosomeLength; // 染色体长度(最大移动步数)

    public GeneticAlgorithmKlondike() {
        this.random = new Random();
        this.populationSize = 200;
        this.maxGenerations = 5000;
        this.crossoverRate = 0.8;
        this.mutationRate = 0.15;
        this.chromosomeLength = 150; // 预估足够长的移动序列
    }

    /**
     * 华容道局面状态
     */
    static class BoardState {
        // board[r][c] 存储该位置 occupiedBy 的棋子ID,0表示空
        int[][] board;
        // 每个棋子的位置和尺寸: [id] -> {row, col, height, width}
        Map<Integer, int[]> pieces;

        BoardState() {
            board = new int[ROWS][COLS];
            pieces = new HashMap<>();
        }

        BoardState deepCopy() {
            BoardState copy = new BoardState();
            for (int r = 0; r < ROWS; r++) {
                copy.board[r] = board[r].clone();
            }
            for (Map.Entry<Integer, int[]> e : pieces.entrySet()) {
                copy.pieces.put(e.getKey(), e.getValue().clone());
            }
            return copy;
        }
    }

    /**
     * 染色体:编码一条移动序列
     * genes[i] = {pieceId, direction}
     */
    static class Chromosome {
        int[][] genes; // [步数][0=棋子ID, 1=方向]
        double fitness;
        int validSteps; // 实际有效步数(遇到死锁前的步数)

        Chromosome(int length) {
            genes = new int[length][2];
            fitness = 0;
            validSteps = 0;
        }
    }

初始化经典华容道布局

    /**
     * 创建经典"横刀立马"初始布局
     */
    private BoardState createInitialBoard() {
        BoardState state = new BoardState();
        // 曹操 2×2 位于 (0,1)
        placePiece(state, 1, CAOCAO, 0, 1, 2, 2);
        // 关羽 1×2 横刀 位于 (2,1)
        placePiece(state, 2, GENERAL, 2, 1, 1, 2);
        // 张飞 2×1 位于 (0,0)
        placePiece(state, 3, GENERAL, 0, 0, 2, 1);
        // 赵云 2×1 位于 (0,3)
        placePiece(state, 4, GENERAL, 0, 3, 2, 1);
        // 马超 2×1 位于 (2,0)
        placePiece(state, 5, GENERAL, 2, 0, 2, 1);
        // 黄忠 2×1 位于 (2,2)
        placePiece(state, 6, GENERAL, 2, 2, 2, 1);
        // 四个小兵
        placePiece(state, 7, SOLDIER, 2, 1, 1, 1); // 注意与关羽位置协调
        placePiece(state, 8, SOLDIER, 4, 0, 1, 1);
        placePiece(state, 9, SOLDIER, 4, 1, 1, 1);
        placePiece(state, 10, SOLDIER, 4, 2, 1, 1);
        // 重新调整小兵以匹配经典布局
        // 经典布局:
        // 张 曹 曹 赵
        // 张 曹 曹 赵
        // 马 关 关 黄
        // 马 兵 兵 黄
        // 兵    兵
        state = new BoardState();
        placePiece(state, 1, CAOCAO, 0, 1, 2, 2);
        placePiece(state, 2, GENERAL, 2, 1, 1, 2); // 关羽横
        placePiece(state, 3, GENERAL, 0, 0, 2, 1); // 张飞竖
        placePiece(state, 4, GENERAL, 0, 3, 2, 1); // 赵云竖
        placePiece(state, 5, GENERAL, 2, 0, 2, 1); // 马超竖
        placePiece(state, 6, GENERAL, 2, 3, 2, 1); // 黄忠竖
        placePiece(state, 7, SOLDIER, 3, 1, 1, 1);
        placePiece(state, 8, SOLDIER, 3, 2, 1, 1);
        placePiece(state, 9, SOLDIER, 4, 0, 1, 1);
        placePiece(state, 10, SOLDIER, 4, 3, 1, 1);
        return state;
    }

    private void placePiece(BoardState state, int id, int type, int r, int c, int h, int w) {
        state.pieces.put(id, new int[]{r, c, h, w});
        for (int i = r; i < r + h; i++) {
            for (int j = c; j < c + w; j++) {
                state.board[i][j] = id;
            }
        }
    }

移动操作与合法性判断

    /**
     * 尝试移动指定棋子到指定方向
     * @return 移动后的新状态,若非法则返回null
     */
    private BoardState tryMove(BoardState state, int pieceId, int dir) {
        int[] piece = state.pieces.get(pieceId);
        if (piece == null) return null;

        int nr = piece[0] + DIRS[dir][0];
        int nc = piece[1] + DIRS[dir][1];
        int h = piece[2];
        int w = piece[3];

        // 边界检查
        if (nr < 0 || nr + h > ROWS || nc < 0 || nc + w > COLS) return null;

        // 目标位置必须全部为空或为自己
        for (int i = nr; i < nr + h; i++) {
            for (int j = nc; j < nc + w; j++) {
                if (state.board[i][j] != 0 && state.board[i][j] != pieceId) {
                    return null; // 与其他棋子冲突
                }
            }
        }

        // 执行移动
        BoardState newState = state.deepCopy();
        // 清空原位置
        for (int i = piece[0]; i < piece[0] + h; i++) {
            for (int j = piece[1]; j < piece[1] + w; j++) {
                newState.board[i][j] = 0;
            }
        }
        // 填充新位置
        for (int i = nr; i < nr + h; i++) {
            for (int j = nc; j < nc + w; j++) {
                newState.board[i][j] = pieceId;
            }
        }
        newState.pieces.put(pieceId, new int[]{nr, nc, h, w});
        return newState;
    }

    /**
     * 检查曹操是否到达出口
     * 出口在底部中央 (3,1) 到 (3,2) 下方
     */
    private boolean isGoal(BoardState state) {
        int[] caocao = state.pieces.get(1);
        return caocao[0] == 3 && caocao[1] == 1; // 曹操底部到达第4行
    }

适应度函数设计

    /**
     * 评估染色体适应度
     * 综合考虑:曹操到出口距离、移动步数、死锁惩罚、通路顺畅度
     */
    private void evaluateFitness(Chromosome chrom, BoardState initial) {
        BoardState current = initial.deepCopy();
        int steps = 0;
        Set<String> visited = new HashSet<>();
        visited.add(boardToString(current));

        for (int i = 0; i < chrom.genes.length; i++) {
            int pieceId = chrom.genes[i][0];
            int dir = chrom.genes[i][1];

            BoardState next = tryMove(current, pieceId, dir);
            if (next == null) {
                // 非法移动,停止模拟
                chrom.validSteps = steps;
                break;
            }

            String stateStr = boardToString(next);
            if (visited.contains(stateStr)) {
                // 循环状态,轻微惩罚但继续
                chrom.fitness -= 0.5;
            }
            visited.add(stateStr);

            current = next;
            steps++;
            chrom.validSteps = steps;

            if (isGoal(current)) {
                // 到达目标!极大奖励
                chrom.fitness = 1000.0 - steps * 0.5;
                return;
            }
        }

        // 未到达目标,基于曹操位置评分
        int[] caocao = current.pieces.get(1);
        // 曹操到出口的距离(出口在底部中央)
        double distToGoal = Math.abs(caocao[0] - 3) + Math.abs(caocao[1] - 1);

        // 下方通路评估:曹操正下方是否有足够空间
        double pathClearance = evaluatePathClearance(current);

        // 综合适应度:距离越近、通路越顺畅,分数越高
        chrom.fitness = 50.0 - distToGoal * 5.0 + pathClearance * 10.0 - steps * 0.1;
    }

    /**
     * 评估曹操到出口的通路顺畅度
     */
    private double evaluatePathClearance(BoardState state) {
        int[] caocao = state.pieces.get(1);
        // 检查曹操正下方两行两列是否畅通
        int clearCount = 0;
        for (int r = caocao[0] + caocao[2]; r < ROWS; r++) {
            for (int c = caocao[1]; c < caocao[1] + caocao[3]; c++) {
                if (r < ROWS && c < COLS && state.board[r][c] == 0) {
                    clearCount++;
                }
            }
        }
        return clearCount / 4.0; // 最大为1.0
    }

    private String boardToString(BoardState state) {
        StringBuilder sb = new StringBuilder();
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                sb.append(state.board[r][c]).append(',');
            }
        }
        return sb.toString();
    }

遗传操作:选择、交叉与变异

    /**
     * 初始化随机种群
     */
    private List<Chromosome> initPopulation() {
        List<Chromosome> pop = new ArrayList<>();
        List<Integer> pieceIds = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9,10));

        for (int i = 0; i < populationSize; i++) {
            Chromosome chrom = new Chromosome(chromosomeLength);
            for (int j = 0; j < chromosomeLength; j++) {
                chrom.genes[j][0] = pieceIds.get(random.nextInt(pieceIds.size()));
                chrom.genes[j][1] = random.nextInt(4);
            }
            pop.add(chrom);
        }
        return pop;
    }

    /**
     * 轮盘赌选择:适应度越高,被选中的概率越大
     */
    private Chromosome selectParent(List<Chromosome> population) {
        double totalFitness = population.stream().mapToDouble(c -> Math.max(c.fitness, 0.1)).sum();
        double threshold = random.nextDouble() * totalFitness;
        double sum = 0;
        for (Chromosome c : population) {
            sum += Math.max(c.fitness, 0.1);
            if (sum >= threshold) return c;
        }
        return population.get(population.size() - 1);
    }

    /**
     * 单点交叉:交换两条染色体从某点开始的基因序列
     */
    private Chromosome[] crossover(Chromosome p1, Chromosome p2) {
        if (random.nextDouble() > crossoverRate) {
            return new Chromosome[]{p1, p2};
        }

        int point = random.nextInt(chromosomeLength);
        Chromosome c1 = new Chromosome(chromosomeLength);
        Chromosome c2 = new Chromosome(chromosomeLength);

        for (int i = 0; i < chromosomeLength; i++) {
            if (i < point) {
                c1.genes[i] = p1.genes[i].clone();
                c2.genes[i] = p2.genes[i].clone();
            } else {
                c1.genes[i] = p2.genes[i].clone();
                c2.genes[i] = p1.genes[i].clone();
            }
        }
        return new Chromosome[]{c1, c2};
    }

    /**
     * 变异:以一定概率随机改变某些基因
     */
    private void mutate(Chromosome chrom) {
        List<Integer> pieceIds = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9,10));
        for (int i = 0; i < chromosomeLength; i++) {
            if (random.nextDouble() < mutationRate) {
                chrom.genes[i][0] = pieceIds.get(random.nextInt(pieceIds.size()));
            }
            if (random.nextDouble() < mutationRate) {
                chrom.genes[i][1] = random.nextInt(4);
            }
        }
    }

主进化循环

    /**
     * 执行遗传算法求解华容道
     * @return 最优解的移动序列
     */
    public List<String> solve() {
        BoardState initial = createInitialBoard();
        List<Chromosome> population = initPopulation();
        Chromosome bestEver = null;

        for (int gen = 0; gen < maxGenerations; gen++) {
            // 评估适应度
            for (Chromosome chrom : population) {
                evaluateFitness(chrom, initial);
                if (bestEver == null || chrom.fitness > bestEver.fitness) {
                    bestEver = chrom;
                    if (chrom.fitness >= 1000.0) {
                        System.out.println("找到解!代数: " + gen);
                        return extractSolution(bestEver, initial);
                    }
                }
            }

            // 排序
            population.sort((a, b) -> Double.compare(b.fitness, a.fitness));

            if (gen % 500 == 0) {
                System.out.println("Generation " + gen + ", Best Fitness: " + 
                    String.format("%.2f", population.get(0).fitness) + 
                    ", Valid Steps: " + population.get(0).validSteps);
            }

            // 生成新一代
            List<Chromosome> newPopulation = new ArrayList<>();
            // 精英保留
            int eliteCount = 10;
            for (int i = 0; i < eliteCount; i++) {
                newPopulation.add(population.get(i));
            }

            while (newPopulation.size() < populationSize) {
                Chromosome p1 = selectParent(population);
                Chromosome p2 = selectParent(population);
                Chromosome[] children = crossover(p1, p2);
                mutate(children[0]);
                mutate(children[1]);
                newPopulation.add(children[0]);
                if (newPopulation.size() < populationSize) {
                    newPopulation.add(children[1]);
                }
            }

            population = newPopulation;
        }

        System.out.println("达到最大代数,最优适应度: " + bestEver.fitness);
        return extractSolution(bestEver, initial);
    }

    /**
     * 从最优染色体提取可执行的移动序列
     */
    private List<String> extractSolution(Chromosome chrom, BoardState initial) {
        List<String> moves = new ArrayList<>();
        BoardState current = initial.deepCopy();
        List<Integer> pieceNames = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        String[] nameMap = {"","曹操","关羽","张飞","赵云","马超","黄忠","兵","兵","兵","兵"};

        for (int i = 0; i < chrom.validSteps; i++) {
            int pieceId = chrom.genes[i][0];
            int dir = chrom.genes[i][1];
            BoardState next = tryMove(current, pieceId, dir);
            if (next != null) {
                moves.add(nameMap[pieceId] + DIR_NAMES[dir]);
                current = next;
                if (isGoal(current)) {
                    moves.add("【曹操到达出口,胜利!】");
                    break;
                }
            }
        }
        return moves;
    }

    public static void main(String[] args) {
        GeneticAlgorithmKlondike solver = new GeneticAlgorithmKlondike();
        List<String> solution = solver.solve();
        System.out.println("\n=== 最优移动序列 ===");
        for (int i = 0; i < solution.size(); i++) {
            System.out.println((i + 1) + ". " + solution.get(i));
        }
    }
}

算法复杂度分析

阶段 时间复杂度 空间复杂度
初始化种群 O(P×L) O(P×L)
适应度评估 O(P×L×R×C) O(R×C)
选择 O(P) O(1)
交叉 O(P×L) O(P×L)
变异 O(P×L) O(1)
单代进化 O(P×L×R×C) O(P×L)
完整求解 O(G×P×L×R×C) O(P×L)

其中 P=种群大小, L=染色体长度, G=代数, R×C=棋盘大小。

优化与改进

  1. 自适应变异率:种群多样性降低时自动提高变异率
  2. 启发式初始化:利用已知开局走法生成部分初始个体
  3. 多步模拟评估:评估染色体时不只模拟单步,而是预测多步后的局面
  4. 并行评估:利用多线程同时评估种群中多个个体

总结

遗传算法为华容道这类组合爆炸问题提供了一种启发式求解路径。通过合理的染色体编码和适应度设计,算法能够在较短时间内找到可行解,且随着迭代不断逼近最优。相比传统搜索算法,遗传算法在超大状态空间中展现出更好的可扩展性。