每日算法 — 使用java实现数独:回溯法与约束传播

引言

数独(Sudoku)是一种源自18世纪瑞士的逻辑填数游戏,玩家需要在9×9的网格中填入数字1-9,使得每行、每列以及每个3×3的宫格内都不出现重复数字。数独的求解是约束满足问题(CSP)的经典案例,看似简单的人类解题过程背后,蕴含着丰富的算法设计空间。本文将使用Java实现数独求解器,核心采用回溯法(Backtracking)进行深度搜索,并结合约束传播(Constraint Propagation)中的唯一候选数法和隐性唯一候选数法进行剪枝,大幅提升求解效率。

问题建模

将数独棋盘表示为9×9的二维数组,空格子用0表示。约束条件为:
– 每行包含1-9且不重复
– 每列包含1-9且不重复
– 每个3×3宫格包含1-9且不重复

状态转移:选择一个空格子,尝试填入1-9中合法的数字,递归求解。

核心算法设计

1. 棋盘表示与基础约束检查

import java.util.*;

/**
 * 数独求解器
 * 结合回溯法与约束传播
 */
public class SudokuSolver {
    private static final int SIZE = 9;
    private static final int BOX_SIZE = 3;

    private int[][] board;
    private boolean solved;

    public SudokuSolver(int[][] board) {
        this.board = new int[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) {
            System.arraycopy(board[i], 0, this.board[i], 0, SIZE);
        }
        this.solved = false;
    }

    public int[][] getBoard() { return board; }

    /**
     * 检查在(row, col)位置填入num是否合法
     */
    public boolean isValid(int row, int col, int num) {
        // 检查行
        for (int c = 0; c < SIZE; c++) {
            if (board[row][c] == num) return false;
        }

        // 检查列
        for (int r = 0; r < SIZE; r++) {
            if (board[r][col] == num) return false;
        }

        // 检查3x3宫格
        int boxRow = (row / BOX_SIZE) * BOX_SIZE;
        int boxCol = (col / BOX_SIZE) * BOX_SIZE;
        for (int r = boxRow; r < boxRow + BOX_SIZE; r++) {
            for (int c = boxCol; c < boxCol + BOX_SIZE; c++) {
                if (board[r][c] == num) return false;
            }
        }

        return true;
    }

    /**
     * 找到下一个空格
     * @return [row, col] 或 null(已全部填满)
     */
    private int[] findEmpty() {
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (board[r][c] == 0) {
                    return new int[]{r, c};
                }
            }
        }
        return null;
    }

2. 基础回溯法求解

    /**
     * 基础回溯法求解
     * 按顺序尝试每个空格的所有候选数
     */
    public boolean solveBacktrack() {
        int[] empty = findEmpty();
        if (empty == null) {
            return true; // 已全部填满
        }

        int row = empty[0];
        int col = empty[1];

        for (int num = 1; num <= SIZE; num++) {
            if (isValid(row, col, num)) {
                board[row][col] = num;

                if (solveBacktrack()) {
                    return true;
                }

                // 回溯
                board[row][col] = 0;
            }
        }

        return false; // 无解
    }

3. MRV启发式与约束传播优化

    /**
     * 计算某空格的可选数字集合(候选数)
     */
    public List<Integer> getCandidates(int row, int col) {
        List<Integer> candidates = new ArrayList<>();
        for (int num = 1; num <= SIZE; num++) {
            if (isValid(row, col, num)) {
                candidates.add(num);
            }
        }
        return candidates;
    }

    /**
     * MRV(Minimum Remaining Values)启发式
     * 选择候选数最少的空格,减少分支因子
     */
    private int[] findEmptyMRV() {
        int minCandidates = SIZE + 1;
        int[] best = null;

        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (board[r][c] == 0) {
                    int count = getCandidates(r, c).size();
                    if (count < minCandidates) {
                        minCandidates = count;
                        best = new int[]{r, c};
                        if (count == 1) return best; // 最优情况
                    }
                }
            }
        }
        return best;
    }

    /**
     * 约束传播:唯一候选数法(Naked Single)
     * 如果某空格只有一个候选数,直接填入
     * @return 是否成功传播(无矛盾)
     */
    public boolean propagateConstraints() {
        boolean changed = true;
        while (changed) {
            changed = false;
            for (int r = 0; r < SIZE; r++) {
                for (int c = 0; c < SIZE; c++) {
                    if (board[r][c] == 0) {
                        List<Integer> candidates = getCandidates(r, c);
                        if (candidates.isEmpty()) {
                            return false; // 矛盾:无候选数
                        }
                        if (candidates.size() == 1) {
                            board[r][c] = candidates.get(0);
                            changed = true;
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * 高级求解:MRV + 约束传播 + 回溯
     */
    public boolean solveAdvanced() {
        // 先进行约束传播简化问题
        if (!propagateConstraints()) {
            return false;
        }

        int[] empty = findEmptyMRV();
        if (empty == null) {
            return true; // 已解决
        }

        int row = empty[0];
        int col = empty[1];
        List<Integer> candidates = getCandidates(row, col);

        for (int num : candidates) {
            // 保存当前棋盘状态(用于回溯)
            int[][] saved = saveState();

            board[row][col] = num;

            if (solveAdvanced()) {
                return true;
            }

            // 恢复状态
            restoreState(saved);
        }

        return false;
    }

    private int[][] saveState() {
        int[][] copy = new int[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) {
            System.arraycopy(board[i], 0, copy[i], 0, SIZE);
        }
        return copy;
    }

    private void restoreState(int[][] state) {
        for (int i = 0; i < SIZE; i++) {
            System.arraycopy(state[i], 0, board[i], 0, SIZE);
        }
    }

4. 数独生成器

    /**
     * 生成完整数独解(随机填充后求解)
     */
    public static int[][] generateCompleteBoard() {
        int[][] empty = new int[SIZE][SIZE];
        SudokuSolver solver = new SudokuSolver(empty);
        solver.fillRandomDiagonal(); // 随机填充对角线三个宫格
        solver.solveAdvanced();
        return solver.getBoard();
    }

    /**
     * 随机填充对角线上的三个3x3宫格
     * 对角线宫格互不干扰,可独立随机填充
     */
    private void fillRandomDiagonal() {
        Random random = new Random();
        for (int box = 0; box < 3; box++) {
            List<Integer> nums = new ArrayList<>();
            for (int i = 1; i <= SIZE; i++) nums.add(i);
            Collections.shuffle(nums, random);

            int idx = 0;
            for (int r = box * BOX_SIZE; r < box * BOX_SIZE + BOX_SIZE; r++) {
                for (int c = box * BOX_SIZE; c < box * BOX_SIZE + BOX_SIZE; c++) {
                    board[r][c] = nums.get(idx++);
                }
            }
        }
    }

    /**
     * 从完整解中挖空生成谜题
     * @param numHoles 挖空数量
     */
    public static int[][] generatePuzzle(int numHoles) {
        int[][] solution = generateCompleteBoard();
        int[][] puzzle = new int[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) {
            System.arraycopy(solution[i], 0, puzzle[i], 0, SIZE);
        }

        Random random = new Random();
        int holes = 0;
        while (holes < numHoles) {
            int r = random.nextInt(SIZE);
            int c = random.nextInt(SIZE);
            if (puzzle[r][c] != 0) {
                puzzle[r][c] = 0;
                holes++;
            }
        }
        return puzzle;
    }

5. 主程序与测试

    /**
     * 打印数独棋盘
     */
    public void printBoard() {
        for (int r = 0; r < SIZE; r++) {
            if (r % BOX_SIZE == 0 && r != 0) {
                System.out.println("------+-------+------");
            }
            for (int c = 0; c < SIZE; c++) {
                if (c % BOX_SIZE == 0 && c != 0) {
                    System.out.print("| ");
                }
                if (board[r][c] == 0) {
                    System.out.print(". ");
                } else {
                    System.out.print(board[r][c] + " ");
                }
            }
            System.out.println();
        }
    }

    /**
     * 验证解的正确性
     */
    public boolean verifySolution() {
        for (int i = 0; i < SIZE; i++) {
            boolean[] rowCheck = new boolean[SIZE + 1];
            boolean[] colCheck = new boolean[SIZE + 1];
            for (int j = 0; j < SIZE; j++) {
                // 检查行
                if (board[i][j] == 0) return false;
                if (rowCheck[board[i][j]]) return false;
                rowCheck[board[i][j]] = true;

                // 检查列
                if (colCheck[board[j][i]]) return false;
                colCheck[board[j][i]] = true;
            }
        }

        // 检查宫格
        for (int boxR = 0; boxR < 3; boxR++) {
            for (int boxC = 0; boxC < 3; boxC++) {
                boolean[] boxCheck = new boolean[SIZE + 1];
                for (int r = boxR * BOX_SIZE; r < boxR * BOX_SIZE + BOX_SIZE; r++) {
                    for (int c = boxC * BOX_SIZE; c < boxC * BOX_SIZE + BOX_SIZE; c++) {
                        if (boxCheck[board[r][c]]) return false;
                        boxCheck[board[r][c]] = true;
                    }
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        // 经典数独难题(世界最难数独之一)
        int[][] hardPuzzle = {
            {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("原始谜题:");
        SudokuSolver solver = new SudokuSolver(hardPuzzle);
        solver.printBoard();

        long start = System.currentTimeMillis();
        boolean success = solver.solveAdvanced();
        long end = System.currentTimeMillis();

        System.out.println("\n求解结果:" + (success ? "成功" : "失败"));
        System.out.println("耗时:" + (end - start) + "ms");

        if (success) {
            System.out.println("\n解答:");
            solver.printBoard();
            System.out.println("验证:" + (solver.verifySolution() ? "正确" : "错误"));
        }

        // 生成随机谜题测试
        System.out.println("\n=== 随机生成测试 ===");
        int[][] puzzle = generatePuzzle(40);
        System.out.println("随机谜题(40空格):");
        SudokuSolver genSolver = new SudokuSolver(puzzle);
        genSolver.printBoard();

        start = System.currentTimeMillis();
        success = genSolver.solveAdvanced();
        end = System.currentTimeMillis();
        System.out.println("\n求解:" + (success ? "成功" : "失败") + ",耗时:" + (end - start) + "ms");
    }
}

复杂度分析

算法 时间复杂度 空间复杂度 说明
基础回溯法 O(9^m) O(81) m为空格数,最坏情况指数级
MRV启发式 O(9^m) O(81) 实际大幅减少分支
约束传播 O(81 × 9) O(81) 多项式时间预处理
组合算法 远低于O(9^m) O(81) 实际求解极快

对于标准9×9数独,组合算法在毫秒级完成求解;即使是世界最难数独,也能在1秒内解决。

算法扩展

  1. ** Dancing Links (DLX)**:Donald Knuth提出的精确覆盖问题算法,将数独转化为精确覆盖后用DLX高效求解
  2. 隐性唯一候选数(Hidden Single):某数字在某行/列/宫格中只有唯一可填位置
  3. 高级约束技术: naked pairs/triples、X-Wing、Swordfish等人类高级技巧
  4. 多解检测:修改回溯条件,收集所有合法解,判断谜题是否唯一解

总结

本文通过回溯法与约束传播的结合实现了高效的数独求解器。核心优化策略包括:
1. MRV启发式:优先选择候选数最少的变量,最小化分支因子
2. 约束传播:在搜索前反复应用唯一候选数规则,大幅简化问题
3. 对角线随机初始化:利用对角线宫格的独立性快速生成完整解

数独求解展示了约束满足问题的经典求解范式:约束传播降低搜索空间,启发式排序优化搜索顺序,回溯法保证完备性。这一组合思路适用于所有CSP问题,如图着色、调度排课、电路布局等。