每日算法 — 使用java实现数独:遗传算法与约束满足混合求解

一、问题背景

数独(Sudoku)是一种经典的逻辑填空游戏,在9×9的格子中填入1-9的数字,要求每行、每列以及每个3×3的宫格内数字均不重复。对于困难数独实例,传统的回溯法可能面临指数级搜索空间。本文用Java实现一个遗传算法(GA)与约束满足(CSP)混合求解器:利用遗传算法的全局搜索能力快速收敛到近似解,再通过约束传播技术修复局部冲突,兼顾效率与解的质量。

二、算法核心思想

2.1 遗传算法编码

将数独的每一行视为一个排列染色体。由于数独规则要求每行恰好包含1-9各一次,染色体天然满足行约束,只需优化列约束和宫格约束的违反数。

2.2 适应度函数

定义适应度为”非重复约束的满足数量”:

$$Fitness = \sum_{j=1}^{9} \text{uniqueCount}(column_j) + \sum_{b=1}^{9} \text{uniqueCount}(box_b)$$

其中 $\text{uniqueCount}$ 统计列或宫格中不重复数字的个数。理想解的适应度为 $9 \times 9 + 9 \times 9 = 162$。

2.3 约束传播辅助

在交叉和变异后,对部分格子应用前向检查(Forward Checking):若某空格在某行中的可选值集(域)大小为1,则直接确定该值。这加速收敛并避免完全随机搜索。

三、完整Java实现

import java.util.*;

/**
 * 数独求解器 - 遗传算法与约束满足混合求解
 */
public class SudokuGA {

    static final int N = 9;
    static final int BOX = 3;
    static final int POP_SIZE = 200;
    static final int MAX_GEN = 50000;
    static final double MUTATE_RATE = 0.15;
    static final double CROSSOVER_RATE = 0.9;
    static final Random rand = new Random(42);

    // ==================== 染色体 ====================
    static class Chromosome {
        int[][] grid; // 完整9x9数独
        int fitness;
        boolean[][] fixed; // 原始给定数字位置不可变

        public Chromosome(int[][] puzzle) {
            grid = new int[N][N];
            fixed = new boolean[N][N];
            for (int r = 0; r < N; r++) {
                for (int c = 0; c < N; c++) {
                    if (puzzle[r][c] != 0) {
                        grid[r][c] = puzzle[r][c];
                        fixed[r][c] = true;
                    }
                }
            }
            // 每行填充为1-9的排列,保持固定数字不变
            for (int r = 0; r < N; r++) {
                boolean[] used = new boolean[N + 1];
                for (int c = 0; c < N; c++) if (fixed[r][c]) used[grid[r][c]] = true;
                List<Integer> pool = new ArrayList<>();
                for (int v = 1; v <= N; v++) if (!used[v]) pool.add(v);
                Collections.shuffle(pool, rand);
                int idx = 0;
                for (int c = 0; c < N; c++) {
                    if (!fixed[r][c]) grid[r][c] = pool.get(idx++);
                }
            }
            evaluate();
        }

        /** 计算适应度:列唯一数 + 宫格唯一数 */
        void evaluate() {
            fitness = 0;
            // 列约束
            for (int c = 0; c < N; c++) {
                boolean[] seen = new boolean[N + 1];
                for (int r = 0; r < N; r++) {
                    if (!seen[grid[r][c]]) { seen[grid[r][c]] = true; fitness++; }
                }
            }
            // 宫格约束
            for (int br = 0; br < N; br += BOX) {
                for (int bc = 0; bc < N; bc += BOX) {
                    boolean[] seen = new boolean[N + 1];
                    for (int r = br; r < br + BOX; r++) {
                        for (int c = bc; c < bc + BOX; c++) {
                            if (!seen[grid[r][c]]) { seen[grid[r][c]] = true; fitness++; }
                        }
                    }
                }
            }
        }

        /** 深拷贝 */
        Chromosome copy() {
            Chromosome ch = new Chromosome(new int[N][N]);
            for (int r = 0; r < N; r++) {
                System.arraycopy(grid[r], 0, ch.grid[r], 0, N);
                System.arraycopy(fixed[r], 0, ch.fixed[r], 0, N);
            }
            ch.fitness = fitness;
            return ch;
        }
    }

    // ==================== 遗传操作 ====================
    static class GA {
        List<Chromosome> population = new ArrayList<>();
        int[][] puzzle;
        int bestFitness = 0;
        Chromosome bestChromo;

        public GA(int[][] puzzle) {
            this.puzzle = puzzle;
            for (int i = 0; i < POP_SIZE; i++) population.add(new Chromosome(puzzle));
            updateBest();
        }

        void updateBest() {
            for (Chromosome ch : population) {
                if (ch.fitness > bestFitness) {
                    bestFitness = ch.fitness;
                    bestChromo = ch.copy();
                }
            }
        }

        /** 锦标赛选择 */
        Chromosome select() {
            int tSize = 5;
            Chromosome best = population.get(rand.nextInt(POP_SIZE));
            for (int i = 1; i < tSize; i++) {
                Chromosome contender = population.get(rand.nextInt(POP_SIZE));
                if (contender.fitness > best.fitness) best = contender;
            }
            return best;
        }

        /** 行级交叉:交换两行的排列 */
        void crossover(Chromosome p1, Chromosome p2, Chromosome c1, Chromosome c2) {
            for (int r = 0; r < N; r++) {
                if (rand.nextDouble() < 0.5) {
                    System.arraycopy(p1.grid[r], 0, c1.grid[r], 0, N);
                    System.arraycopy(p2.grid[r], 0, c2.grid[r], 0, N);
                } else {
                    System.arraycopy(p2.grid[r], 0, c1.grid[r], 0, N);
                    System.arraycopy(p1.grid[r], 0, c2.grid[r], 0, N);
                }
                // 恢复固定数字
                for (int c = 0; c < N; c++) {
                    if (puzzle[r][c] != 0) {
                        c1.grid[r][c] = puzzle[r][c];
                        c2.grid[r][c] = puzzle[r][c];
                    }
                }
            }
        }

        /** 变异:对非固定行进行局部打乱(2-opt变异) */
        void mutate(Chromosome ch) {
            for (int r = 0; r < N; r++) {
                if (rand.nextDouble() > MUTATE_RATE) continue;
                // 找到该行中两个可变的列进行交换
                List<Integer> cols = new ArrayList<>();
                for (int c = 0; c < N; c++) if (!ch.fixed[r][c]) cols.add(c);
                if (cols.size() >= 2) {
                    int i1 = rand.nextInt(cols.size());
                    int i2 = rand.nextInt(cols.size());
                    int c1 = cols.get(i1), c2 = cols.get(i2);
                    int tmp = ch.grid[r][c1];
                    ch.grid[r][c1] = ch.grid[r][c2];
                    ch.grid[r][c2] = tmp;
                }
            }
        }

        /** 约束传播:前向检查修复单列/单宫冲突 */
        void forwardCheck(Chromosome ch) {
            // 对每列,如果某数字只出现在一个可变的行中,尝试固定
            for (int c = 0; c < N; c++) {
                for (int val = 1; val <= N; val++) {
                    int count = 0, pos = -1;
                    for (int r = 0; r < N; r++) {
                        if (ch.grid[r][c] == val) { count++; }
                    }
                    if (count > 1) {
                        // 存在冲突,随机保留一个,其余行尝试交换
                        List<Integer> rows = new ArrayList<>();
                        for (int r = 0; r < N; r++) if (ch.grid[r][c] == val && !ch.fixed[r][c]) rows.add(r);
                        if (rows.size() > 1) {
                            // 随机选择一个冲突行,与该行中可交换的位置交换
                            int r = rows.get(rand.nextInt(rows.size()));
                            List<Integer> swapCols = new ArrayList<>();
                            for (int cc = 0; cc < N; cc++) if (!ch.fixed[r][cc] && cc != c) swapCols.add(cc);
                            if (!swapCols.isEmpty()) {
                                int sc = swapCols.get(rand.nextInt(swapCols.size()));
                                int tmp = ch.grid[r][c];
                                ch.grid[r][c] = ch.grid[r][sc];
                                ch.grid[r][sc] = tmp;
                            }
                        }
                    }
                }
            }
        }

        /** 进化一代 */
        void evolve() {
            List<Chromosome> newPop = new ArrayList<>();
            while (newPop.size() < POP_SIZE) {
                Chromosome p1 = select();
                Chromosome p2 = select();
                Chromosome c1 = new Chromosome(puzzle);
                Chromosome c2 = new Chromosome(puzzle);
                if (rand.nextDouble() < CROSSOVER_RATE) {
                    crossover(p1, p2, c1, c2);
                } else {
                    for (int r = 0; r < N; r++) {
                        System.arraycopy(p1.grid[r], 0, c1.grid[r], 0, N);
                        System.arraycopy(p2.grid[r], 0, c2.grid[r], 0, N);
                    }
                }
                mutate(c1);
                mutate(c2);
                forwardCheck(c1);
                forwardCheck(c2);
                c1.evaluate();
                c2.evaluate();
                newPop.add(c1);
                newPop.add(c2);
            }
            population = newPop.subList(0, POP_SIZE);
            updateBest();
        }

        /** 主求解循环 */
        Chromosome solve() {
            final int TARGET = N * N + N * N; // 162
            for (int gen = 0; gen < MAX_GEN; gen++) {
                evolve();
                if (bestFitness >= TARGET) {
                    System.out.println("找到最优解!代数: " + gen);
                    return bestChromo;
                }
                if (gen % 1000 == 0) {
                    System.out.printf("Gen %d, Best Fitness: %d / %d%n", gen, bestFitness, TARGET);
                }
            }
            System.out.println("达到最大代数,返回最优近似解。Fitness: " + bestFitness);
            return bestChromo;
        }
    }

    // ==================== 主程序 ====================
    public static void main(String[] args) {
        // 一道专家级数独题目(0表示空格)
        int[][] puzzle = {
            {8,0,0, 0,0,0, 0,0,0},
            {0,0,3, 6,0,0, 0,0,0},
            {0,7,0, 0,9,0, 2,0,0},
            {0,5,0, 0,0,7, 0,0,0},
            {0,0,0, 0,4,5, 7,0,0},
            {0,0,0, 1,0,0, 0,3,0},
            {0,0,1, 0,0,0, 0,6,8},
            {0,0,8, 5,0,0, 0,1,0},
            {0,9,0, 0,0,0, 4,0,0},
        };

        System.out.println("原始数独:");
        printGrid(puzzle);

        GA ga = new GA(puzzle);
        Chromosome solution = ga.solve();

        System.out.println("\n求解结果:");
        printGrid(solution.grid);
        System.out.println("最终适应度: " + solution.fitness + " / 162");

        // 验证解的合法性
        if (isValid(solution.grid)) {
            System.out.println("验证通过:解合法!");
        } else {
            System.out.println("验证失败:存在冲突。");
        }
    }

    static boolean isValid(int[][] grid) {
        // 检查行
        for (int r = 0; r < N; r++) {
            boolean[] seen = new boolean[N + 1];
            for (int c = 0; c < N; c++) {
                if (grid[r][c] < 1 || grid[r][c] > 9) return false;
                if (seen[grid[r][c]]) return false;
                seen[grid[r][c]] = true;
            }
        }
        // 检查列
        for (int c = 0; c < N; c++) {
            boolean[] seen = new boolean[N + 1];
            for (int r = 0; r < N; r++) {
                if (seen[grid[r][c]]) return false;
                seen[grid[r][c]] = true;
            }
        }
        // 检查宫格
        for (int br = 0; br < N; br += BOX) {
            for (int bc = 0; bc < N; bc += BOX) {
                boolean[] seen = new boolean[N + 1];
                for (int r = br; r < br + BOX; r++) {
                    for (int c = bc; c < bc + BOX; c++) {
                        if (seen[grid[r][c]]) return false;
                        seen[grid[r][c]] = true;
                    }
                }
            }
        }
        return true;
    }

    static void printGrid(int[][] grid) {
        for (int r = 0; r < N; r++) {
            if (r % BOX == 0) System.out.println("+-------+-------+-------+");
            for (int c = 0; c < N; c++) {
                if (c % BOX == 0) System.out.print("| ");
                System.out.print(grid[r][c] == 0 ? ". " : grid[r][c] + " ");
            }
            System.out.println("|");
        }
        System.out.println("+-------+-------+-------+");
    }
}

四、算法优化与扩展

  1. 自适应变异率:当种群适应度停滞时自动提高变异率,逃离局部最优
  2. 精英保留:每代将最优个体直接复制到下一代,防止优秀基因丢失
  3. 模拟退火变异:以一定概率接受劣化解,避免早熟收敛
  4. 与回溯法混合:遗传算法找到近似解后,对剩余冲突区域使用回溯法精确求解

五、算法复杂度分析

操作 时间复杂度 空间复杂度
种群初始化 $O(P \cdot N^2)$ $O(P \cdot N^2)$
适应度评估 $O(N^2)$ $O(N)$
选择 $O(T)$(T为锦标赛大小) $O(1)$
交叉 $O(N^2)$ $O(N^2)$
前向检查 $O(N^3)$ $O(N)$
单代进化 $O(P \cdot N^3)$ $O(P \cdot N^2)$

其中 $P=200$ 为种群大小,$N=9$ 为格子边长。

六、总结

本文实现了遗传算法与约束满足混合的数独求解器:
– 行排列编码:将每行编码为1-9排列,天然满足行约束,缩小搜索空间
– 适应度函数:以列和宫格的唯一数字数量作为优化目标
– 前向检查:在遗传操作后对冲突进行局部修复,加速收敛
– 混合策略:结合进化算法的全局搜索与CSP的局部推理

对于困难数独实例,该方法通常能在数千代内找到精确解,远快于纯回溯法的暴力搜索。