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

游戏介绍

数独(Sudoku)是一种逻辑填数游戏,在9×9的方格中填入1-9的数字,要求每行、每列以及每个3×3的宫格内数字均不重复。本文用Java实现数独求解器,核心使用回溯算法配合约束传播(MRV启发式)高效剪枝。

核心算法

回溯算法(Backtracking)

深度优先搜索尝试每个空格的所有可能数字,一旦发现矛盾则回溯。基础回溯即可解决大多数数独,但效率较低。

MRV启发式(Minimum Remaining Values)

每次选择候选数字最少的空格进行填充,最大化剪枝效果。配合前向检查(Forward Checking),在赋值后立即更新相关单元格的候选集。

约束传播

通过维护每个空格的可选值集合,当某单元格只剩一个候选值时自动确定(单元传播),连锁触发更多确定,大幅减少搜索分支。

Java实现

import java.util.*;

public class SudokuSolver {
    static final int SIZE = 9;
    static final int BOX = 3;

    // 示例谜题(0表示空格)
    static final int[][] PUZZLE = {
        {5,3,0, 0,7,0, 0,0,0},
        {6,0,0, 1,9,5, 0,0,0},
        {0,9,8, 0,0,0, 0,6,0},
        {8,0,0, 0,6,0, 0,0,3},
        {4,0,0, 8,0,3, 0,0,1},
        {7,0,0, 0,2,0, 0,0,6},
        {0,6,0, 0,0,0, 2,8,0},
        {0,0,0, 4,1,9, 0,0,5},
        {0,0,0, 0,8,0, 0,7,9}
    };

    int[][] board = new int[SIZE][SIZE];
    // 每个格子的候选值集合
    List<Set<Integer>> candidates = new ArrayList<>();
    int steps = 0;

    public SudokuSolver() {
        for (int y = 0; y < SIZE; y++) {
            System.arraycopy(PUZZLE[y], 0, board[y], 0, SIZE);
        }
        initCandidates();
    }

    // 初始化候选值
    void initCandidates() {
        for (int i = 0; i < SIZE * SIZE; i++) {
            candidates.add(new HashSet<>());
        }
        for (int y = 0; y < SIZE; y++) {
            for (int x = 0; x < SIZE; x++) {
                if (board[y][x] == 0) {
                    candidates.get(idx(x, y)).addAll(getCandidates(x, y));
                }
            }
        }
    }

    int idx(int x, int y) { return y * SIZE + x; }

    // 计算某空格的所有合法候选值
    Set<Integer> getCandidates(int x, int y) {
        Set<Integer> nums = new HashSet<>();
        for (int i = 1; i <= SIZE; i++) nums.add(i);

        // 移除行中已有的数字
        for (int cx = 0; cx < SIZE; cx++) {
            if (board[y][cx] != 0) nums.remove(board[y][cx]);
        }
        // 移除列中已有的数字
        for (int cy = 0; cy < SIZE; cy++) {
            if (board[cy][x] != 0) nums.remove(board[cy][x]);
        }
        // 移除3x3宫中已有的数字
        int bx = (x / BOX) * BOX, by = (y / BOX) * BOX;
        for (int dy = 0; dy < BOX; dy++) {
            for (int dx = 0; dx < BOX; dx++) {
                int val = board[by + dy][bx + dx];
                if (val != 0) nums.remove(val);
            }
        }
        return nums;
    }

    // MRV:找到候选值最少的空格
    int[] findMRV() {
        int minCount = SIZE + 1;
        int[] best = null;
        for (int y = 0; y < SIZE; y++) {
            for (int x = 0; x < SIZE; x++) {
                if (board[y][x] == 0) {
                    int count = candidates.get(idx(x, y)).size();
                    if (count < minCount) {
                        minCount = count;
                        best = new int[]{x, y};
                        if (count == 1) return best; // 无法更优
                    }
                }
            }
        }
        return best;
    }

    // 回溯求解
    boolean solve() {
        int[] mrv = findMRV();
        if (mrv == null) return true; // 全部填满

        int x = mrv[0], y = mrv[1];
        Set<Integer> opts = new HashSet<>(candidates.get(idx(x, y)));

        for (int val : opts) {
            steps++;
            // 保存状态用于回溯
            Map<Integer, Set<Integer>> changed = new HashMap<>();

            // 放置数字
            board[y][x] = val;
            candidates.get(idx(x, y)).clear();

            // 更新约束:行、列、宫
            boolean valid = true;
            List<int[]> toUpdate = new ArrayList<>();
            for (int cx = 0; cx < SIZE; cx++) {
                if (cx != x && board[y][cx] == 0) toUpdate.add(new int[]{cx, y});
            }
            for (int cy = 0; cy < SIZE; cy++) {
                if (cy != y && board[cy][x] == 0) toUpdate.add(new int[]{x, cy});
            }
            int bx = (x / BOX) * BOX, by = (y / BOX) * BOX;
            for (int dy = 0; dy < BOX; dy++) {
                for (int dx = 0; dx < BOX; dx++) {
                    int nx = bx + dx, ny = by + dy;
                    if ((nx != x || ny != y) && board[ny][nx] == 0) {
                        toUpdate.add(new int[]{nx, ny});
                    }
                }
            }

            for (int[] p : toUpdate) {
                int pi = idx(p[0], p[1]);
                Set<Integer> cands = candidates.get(pi);
                if (cands.contains(val)) {
                    if (!changed.containsKey(pi)) {
                        changed.put(pi, new HashSet<>(cands));
                    }
                    cands.remove(val);
                    if (cands.isEmpty()) {
                        valid = false; // 约束冲突
                        break;
                    }
                }
            }

            // 单元传播:检查是否有唯一候选值的格子
            if (valid) {
                Queue<int[]> queue = new LinkedList<>();
                for (int[] p : toUpdate) {
                    if (candidates.get(idx(p[0], p[1])).size() == 1) {
                        queue.add(p);
                    }
                }
                while (!queue.isEmpty() && valid) {
                    int[] p = queue.poll();
                    if (board[p[1]][p[0]] != 0) continue;
                    Set<Integer> c = candidates.get(idx(p[0], p[1]));
                    if (c.size() != 1) continue;
                    int v = c.iterator().next();
                    // 递归赋值(简化:仅检查是否合法)
                    board[p[1]][p[0]] = v;
                    c.clear();
                    // 继续传播...
                }
            }

            if (valid && solve()) return true;

            // 回溯
            board[y][x] = 0;
            for (Map.Entry<Integer, Set<Integer>> e : changed.entrySet()) {
                candidates.set(e.getKey(), e.getValue());
            }
            // 恢复被单元传播赋值的格子
            for (int[] p : toUpdate) {
                if (board[p[1]][p[0]] != 0 && PUZZLE[p[1]][p[0]] == 0) {
                    // 检查是否是本次传播赋值的
                    boolean fromPuzzle = false;
                    for (int py = 0; py < SIZE; py++) {
                        for (int px = 0; px < SIZE; px++) {
                            if (PUZZLE[py][px] != 0) {
                                // 保留原始谜题的值
                            }
                        }
                    }
                }
            }
            // 简化的回溯:重新计算所有候选值
            restoreCandidates();
        }
        return false;
    }

    // 从当前board重新计算所有候选值
    void restoreCandidates() {
        for (int i = 0; i < SIZE * SIZE; i++) {
            candidates.get(i).clear();
        }
        for (int y = 0; y < SIZE; y++) {
            for (int x = 0; x < SIZE; x++) {
                if (board[y][x] == 0) {
                    candidates.get(idx(x, y)).addAll(getCandidates(x, y));
                }
            }
        }
    }

    // 打印数独
    void printBoard() {
        for (int y = 0; y < SIZE; y++) {
            if (y % BOX == 0 && y > 0) System.out.println("------+-------+------");
            for (int x = 0; x < SIZE; x++) {
                if (x % BOX == 0 && x > 0) System.out.print("| ");
                int v = board[y][x];
                System.out.print(v == 0 ? ". " : v + " ");
            }
            System.out.println();
        }
    }

    // 验证解的正确性
    boolean verify() {
        for (int y = 0; y < SIZE; y++) {
            Set<Integer> row = new HashSet<>();
            for (int x = 0; x < SIZE; x++) {
                if (board[y][x] == 0) return false;
                if (!row.add(board[y][x])) return false;
            }
        }
        for (int x = 0; x < SIZE; x++) {
            Set<Integer> col = new HashSet<>();
            for (int y = 0; y < SIZE; y++) {
                if (!col.add(board[y][x])) return false;
            }
        }
        for (int by = 0; by < SIZE; by += BOX) {
            for (int bx = 0; bx < SIZE; bx += BOX) {
                Set<Integer> box = new HashSet<>();
                for (int dy = 0; dy < BOX; dy++) {
                    for (int dx = 0; dx < BOX; dx++) {
                        if (!box.add(board[by + dy][bx + dx])) return false;
                    }
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        SudokuSolver solver = new SudokuSolver();
        System.out.println("原始谜题:");
        solver.printBoard();

        long start = System.currentTimeMillis();
        boolean solved = solver.solve();
        long end = System.currentTimeMillis();

        System.out.println("\n解答:");
        solver.printBoard();
        System.out.println("求解成功: " + solved);
        System.out.println("验证通过: " + solver.verify());
        System.out.println("搜索步数: " + solver.steps);
        System.out.println("耗时: " + (end - start) + "ms");
    }
}

复杂度分析

  • 基础回溯:最坏情况 $O(9^{81}) \approx O(10^{77})$,不实用
  • MRV+约束传播:通过剪枝将实际复杂度降至 $O(10^4)$ 到 $O(10^6)$ 步
  • 空间复杂度:$O(81 \times 9)$,存储每个格子的候选值集合
  • ** Dancing Links(DLX)**:Donald Knuth提出的精确覆盖算法,可将数独转化为精确覆盖问题,求解效率更高

进阶方向

  1. DLX算法:将数独建模为精确覆盖问题,用舞蹈链数据结构高效求解
  2. 生成谜题:通过随机挖空并确保唯一解来生成数独题目
  3. 难度评级:基于求解所需回溯步数评估题目难度

总结

本文通过MRV启发式和约束传播大幅优化了数独回溯求解的效率。核心思路是”优先处理约束最强的变量”,这一策略在约束满足问题(CSP)中具有普适性。读者可以尝试实现更完善的单元传播和DLX算法,体验不同求解策略的差异。