一、问题背景
数独(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("+-------+-------+-------+");
}
}
四、算法优化与扩展
- 自适应变异率:当种群适应度停滞时自动提高变异率,逃离局部最优
- 精英保留:每代将最优个体直接复制到下一代,防止优秀基因丢失
- 模拟退火变异:以一定概率接受劣化解,避免早熟收敛
- 与回溯法混合:遗传算法找到近似解后,对剩余冲突区域使用回溯法精确求解
五、算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 种群初始化 | $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的局部推理
对于困难数独实例,该方法通常能在数千代内找到精确解,远快于纯回溯法的暴力搜索。