每日算法 — 使用java实现八皇后:遗传算法进化搜索与适应度函数设计

引言

八皇后问题是由国际象棋棋手马克斯·贝塞尔于1848年提出的经典组合优化问题:在8×8的国际象棋棋盘上放置8个皇后,使得任意两个皇后都不能在同一行、同一列或同一对角线上互相攻击。该问题共有92个不同的解。传统的回溯法可以系统地枚举所有解,但当棋盘规模增大时,搜索空间呈指数级增长。本文将使用Java实现基于遗传算法(Genetic Algorithm, GA)的八皇后问题求解器,通过模拟生物进化过程高效搜索可行解。

核心算法思路

1. 编码方案

采用一维数组编码:第i个元素表示第i行皇后所在的列号。例如[0, 4, 7, 5, 2, 6, 1, 3]表示第0行皇后在第0列,第1行皇后在第4列,以此类推。这种编码天然保证每行恰好一个皇后,将搜索空间从8^8缩小到8!。

2. 适应度函数设计

适应度函数衡量个体接近解的程度。定义冲突数为互相攻击的皇后对数(范围为0到28)。适应度值为:fitness = 28 - conflicts。当fitness=28时,即为完美解。

3. 遗传算子

  • 选择:采用锦标赛选择(Tournament Selection),每次随机选取k个个体,选择其中适应度最高的进入下一代;
  • 交叉:使用部分映射交叉(PMX, Partially Mapped Crossover),保持排列的合法性;
  • 变异:采用交换变异(Swap Mutation),随机交换两个位置的列号。

4. 精英保留策略

每代保留最优个体直接进入下一代,防止优秀基因在交叉和变异中丢失。

完整Java实现

import java.util.*;

/**
 * 八皇后问题求解器:遗传算法进化搜索
 */
public class NQueensGA {

    private static final int N = 8;
    private static final int POP_SIZE = 100;
    private static final int MAX_GENERATIONS = 10000;
    private static final double CROSSOVER_RATE = 0.85;
    private static final double MUTATION_RATE = 0.15;
    private static final int TOURNAMENT_SIZE = 5;
    private static final int ELITE_COUNT = 2;

    private Random random;
    private List<Individual> population;
    private int generation;
    private Individual bestEver;

    public NQueensGA() {
        this.random = new Random();
        this.population = new ArrayList<>();
        initializePopulation();
    }

    /**
     * 初始化种群:随机生成合法排列
     */
    private void initializePopulation() {
        for (int i = 0; i < POP_SIZE; i++) {
            int[] chromosome = new int[N];
            List<Integer> cols = new ArrayList<>();
            for (int j = 0; j < N; j++) cols.add(j);
            Collections.shuffle(cols, random);
            for (int j = 0; j < N; j++) chromosome[j] = cols.get(j);
            population.add(new Individual(chromosome));
        }
        bestEver = Collections.max(population, Comparator.comparingInt(a -> a.fitness));
    }

    /**
     * 个体类:包含染色体和适应度
     */
    static class Individual {
        int[] chromosome;
        int fitness;
        int conflicts;

        Individual(int[] chromo) {
            this.chromosome = chromo.clone();
            calculateFitness();
        }

        /**
         * 计算适应度:28 - 冲突对数
         */
        void calculateFitness() {
            this.conflicts = 0;
            for (int i = 0; i < N; i++) {
                for (int j = i + 1; j < N; j++) {
                    // 同一列
                    if (chromosome[i] == chromosome[j]) conflicts++;
                    // 同一对角线
                    if (Math.abs(chromosome[i] - chromosome[j]) == Math.abs(i - j)) conflicts++;
                }
            }
            this.fitness = 28 - conflicts;
        }

        Individual copy() {
            return new Individual(chromosome);
        }

        @Override
        public String toString() {
            return "Fitness=" + fitness + ", Conflicts=" + conflicts + 
                   ", Chromo=" + Arrays.toString(chromosome);
        }
    }

    /**
     * 锦标赛选择
     */
    private Individual tournamentSelection() {
        Individual best = null;
        for (int i = 0; i < TOURNAMENT_SIZE; i++) {
            Individual candidate = population.get(random.nextInt(POP_SIZE));
            if (best == null || candidate.fitness > best.fitness) {
                best = candidate;
            }
        }
        return best.copy();
    }

    /**
     * 部分映射交叉(PMX)
     */
    private Individual[] pmxCrossover(Individual p1, Individual p2) {
        if (random.nextDouble() > CROSSOVER_RATE) {
            return new Individual[]{p1.copy(), p2.copy()};
        }

        int[] c1 = new int[N];
        int[] c2 = new int[N];
        Arrays.fill(c1, -1);
        Arrays.fill(c2, -1);

        // 选择交叉点
        int point1 = random.nextInt(N);
        int point2 = random.nextInt(N);
        if (point1 > point2) {
            int tmp = point1; point1 = point2; point2 = tmp;
        }

        // 复制映射段
        for (int i = point1; i <= point2; i++) {
            c1[i] = p2.chromosome[i];
            c2[i] = p1.chromosome[i];
        }

        // 填充剩余位置,解决冲突
        fillPmxChild(c1, p1.chromosome, point1, point2);
        fillPmxChild(c2, p2.chromosome, point1, point2);

        return new Individual[]{new Individual(c1), new Individual(c2)};
    }

    private void fillPmxChild(int[] child, int[] parent, int p1, int p2) {
        Set<Integer> used = new HashSet<>();
        for (int i = p1; i <= p2; i++) used.add(child[i]);

        for (int i = 0; i < N; i++) {
            if (i >= p1 && i <= p2) continue;
            int val = parent[i];
            while (used.contains(val)) {
                // 查找映射
                for (int j = p1; j <= p2; j++) {
                    if (child[j] == val) {
                        val = parent[j];
                        break;
                    }
                }
            }
            child[i] = val;
            used.add(val);
        }
    }

    /**
     * 交换变异
     */
    private void mutate(Individual ind) {
        if (random.nextDouble() > MUTATION_RATE) return;

        int idx1 = random.nextInt(N);
        int idx2 = random.nextInt(N);
        int tmp = ind.chromosome[idx1];
        ind.chromosome[idx1] = ind.chromosome[idx2];
        ind.chromosome[idx2] = tmp;
        ind.calculateFitness();
    }

    /**
     * 进化一代
     */
    private void evolveOneGeneration() {
        List<Individual> newPopulation = new ArrayList<>();

        // 精英保留
        population.sort((a, b) -> b.fitness - a.fitness);
        for (int i = 0; i < ELITE_COUNT && i < population.size(); i++) {
            newPopulation.add(population.get(i).copy());
        }

        // 生成新个体
        while (newPopulation.size() < POP_SIZE) {
            Individual parent1 = tournamentSelection();
            Individual parent2 = tournamentSelection();
            Individual[] children = pmxCrossover(parent1, parent2);

            mutate(children[0]);
            mutate(children[1]);

            newPopulation.add(children[0]);
            if (newPopulation.size() < POP_SIZE) {
                newPopulation.add(children[1]);
            }
        }

        population = newPopulation;
        generation++;

        // 更新历史最优
        Individual currentBest = Collections.max(population, Comparator.comparingInt(a -> a.fitness));
        if (currentBest.fitness > bestEver.fitness) {
            bestEver = currentBest.copy();
        }
    }

    /**
     * 运行遗传算法直到找到解或达到最大代数
     */
    public Individual solve() {
        long startTime = System.currentTimeMillis();

        while (generation < MAX_GENERATIONS) {
            evolveOneGeneration();

            if (bestEver.fitness == 28) {
                long elapsed = System.currentTimeMillis() - startTime;
                System.out.println("Solution found at generation " + generation + 
                                   " in " + elapsed + "ms");
                return bestEver;
            }

            if (generation % 500 == 0) {
                System.out.println("Gen " + generation + ": Best fitness = " + 
                                   bestEver.fitness + ", Conflicts = " + bestEver.conflicts);
            }
        }

        System.out.println("Max generations reached. Best fitness = " + bestEver.fitness);
        return bestEver;
    }

    /**
     * 可视化打印棋盘
     */
    public void printBoard(Individual solution) {
        System.out.println("\nSolution: " + Arrays.toString(solution.chromosome));
        for (int row = 0; row < N; row++) {
            for (int col = 0; col < N; col++) {
                System.out.print(solution.chromosome[row] == col ? "Q " : ". ");
            }
            System.out.println();
        }
    }

    /**
     * 统计并打印所有不同解(运行多次)
     */
    public void findAllSolutions(int attempts) {
        Set<String> uniqueSolutions = new HashSet<>();
        int totalGenerations = 0;

        for (int i = 0; i < attempts; i++) {
            NQueensGA ga = new NQueensGA();
            Individual sol = ga.solve();
            if (sol.fitness == 28) {
                String key = Arrays.toString(sol.chromosome);
                uniqueSolutions.add(key);
                totalGenerations += ga.generation;
            }
        }

        System.out.println("\n=== Summary ===");
        System.out.println("Attempts: " + attempts);
        System.out.println("Unique solutions found: " + uniqueSolutions.size());
        System.out.println("Avg generations per solution: " + 
                           (uniqueSolutions.size() > 0 ? totalGenerations / uniqueSolutions.size() : 0));
    }

    public static void main(String[] args) {
        // 单次求解
        System.out.println("=== Single Run ===");
        NQueensGA ga = new NQueensGA();
        Individual solution = ga.solve();
        ga.printBoard(solution);

        // 多次运行寻找不同解
        System.out.println("\n=== Finding Multiple Solutions ===");
        ga.findAllSolutions(50);
    }
}

算法复杂度分析

操作 时间复杂度 空间复杂度
适应度计算 O(N²) O(1)
锦标赛选择 O(K) = O(1) O(1)
PMX交叉 O(N²) O(N)
交换变异 O(1) O(1)
单代进化 O(P × N²) O(P × N)
完整求解 O(G × P × N²) O(P × N)

其中N=8为皇后数量,P=100为种群大小,G为实际进化代数(通常几百到几千代),K=5为锦标赛规模。

策略总结

本文通过遗传算法求解八皇后问题,核心设计要点包括:

  1. 排列编码:使用一维数组编码,天然满足每行一个皇后的约束,将搜索空间从16,777,216缩小到40,320;
  2. 冲突最小化适应度:以28减去互相攻击的皇后对数作为适应度,目标明确且计算高效;
  3. PMX交叉算子:保持排列合法性,避免传统交叉产生的重复列号;
  4. 精英保留:每代保留最优个体,确保优秀基因不会丢失;
  5. 交换变异:维持排列性质的同时引入局部扰动,帮助跳出局部最优。

遗传算法的优势在于不依赖问题的梯度信息,通过种群并行搜索多个区域。在八皇后问题上,通常能在数百代内找到最优解,平均耗时仅几十毫秒。相比回溯法的系统性枚举,遗传算法更适合大规模N皇后问题(如N=100以上),此时回溯法已无法在合理时间内求解。