1. 概述
本文将探讨数独谜题及其求解算法。我们将用Java实现两种解决方案:第一种是简单的暴力回溯算法,第二种采用舞蹈链(Dancing Links)技术。需要说明的是,我们将重点关注算法实现而非面向对象设计。
2. 数独谜题
数独是一个9×9单元格的组合数字谜题,部分格子填有1-9的数字。目标是用剩余数字填充空白格子,使得:
- 每行每列都包含1-9且不重复
- 每个3×3子网格内数字也不重复
谜题难度随空白格子数量增加而提升。
2.1 测试棋盘
为验证算法有效性,我们采用**"世界最难数独"**作为测试用例:
8 . . . . . . . .
. . 3 6 . . . . .
. 7 . . 9 . 2 . .
. 5 . . . 7 . . .
. . . . 4 5 7 . .
. . . 1 . . . 3 .
. . 1 . . . . 6 8
. . 8 5 . . . 1 .
. 9 . . . . 4 . .
2.2 解析结果
提前揭晓答案,正确解法如下:
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2
3. 回溯算法
3.1 算法原理
回溯算法通过逐格尝试所有可能解来求解数独:
- ✅ 若当前数字不违反约束,则继续填充下一格
- ❌ 若违反约束,则尝试下一个数字
- ⚠️ 若当前格子所有数字都无效,则回溯到上一格修改数字
本质是暴力搜索所有可能解,但通过剪枝优化效率。
3.2 实现方案
首先用二维数组表示棋盘,0表示空格:
int[][] board = {
{ 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 }
};
核心求解方法:
private boolean solve(int[][] board) {
for (int row = BOARD_START_INDEX; row < BOARD_SIZE; row++) {
for (int column = BOARD_START_INDEX; column < BOARD_SIZE; column++) {
if (board[row][column] == NO_VALUE) {
for (int k = MIN_VALUE; k <= MAX_VALUE; k++) {
board[row][column] = k;
if (isValid(board, row, column) && solve(board)) {
return true;
}
board[row][column] = NO_VALUE;
}
return false;
}
}
}
return true;
}
约束检查方法:
private boolean isValid(int[][] board, int row, int column) {
return (rowConstraint(board, row)
&& columnConstraint(board, column)
&& subsectionConstraint(board, row, column));
}
行约束检查:
private boolean rowConstraint(int[][] board, int row) {
boolean[] constraint = new boolean[BOARD_SIZE];
return IntStream.range(BOARD_START_INDEX, BOARD_SIZE)
.allMatch(column -> checkConstraint(board, row, constraint, column));
}
列约束检查:
private boolean columnConstraint(int[][] board, int column) {
boolean[] constraint = new boolean[BOARD_SIZE];
return IntStream.range(BOARD_START_INDEX, BOARD_SIZE)
.allMatch(row -> checkConstraint(board, row, constraint, column));
}
3×3子网格约束检查:
private boolean subsectionConstraint(int[][] board, int row, int column) {
boolean[] constraint = new boolean[BOARD_SIZE];
int subsectionRowStart = (row / SUBSECTION_SIZE) * SUBSECTION_SIZE;
int subsectionRowEnd = subsectionRowStart + SUBSECTION_SIZE;
int subsectionColumnStart = (column / SUBSECTION_SIZE) * SUBSECTION_SIZE;
int subsectionColumnEnd = subsectionColumnStart + SUBSECTION_SIZE;
for (int r = subsectionRowStart; r < subsectionRowEnd; r++) {
for (int c = subsectionColumnStart; c < subsectionColumnEnd; c++) {
if (!checkConstraint(board, r, constraint, c)) return false;
}
}
return true;
}
通用约束检查方法:
boolean checkConstraint(
int[][] board,
int row,
boolean[] constraint,
int column) {
if (board[row][column] != NO_VALUE) {
if (!constraint[board[row][column] - 1]) {
constraint[board[row][column] - 1] = true;
} else {
return false;
}
}
return true;
}
打印棋盘方法(非算法核心):
private void printBoard() {
for (int row = BOARD_START_INDEX; row < BOARD_SIZE; row++) {
for (int column = BOARD_START_INDEX; column < BOARD_SIZE; column++) {
System.out.print(board[row][column] + " ");
}
System.out.println();
}
}
⚠️ 踩坑提示:虽然算法能正确求解,但效率不高——它会重复检查已知无效的解法。
4. 舞蹈链(Dancing Links)
4.1 精确覆盖问题
数独可转化为精确覆盖问题,即从集合中选出子集,使每个元素恰好出现一次。例如:
- 集合 S = {A, B, C, D, E, F}
- A = {1,4,7}, B={1,4}, C={4,5,7}, D={3,5,6}, E={2,3,6,7}, F={2,7}
用矩阵表示(行=集合,列=元素):
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
A | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
C | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
E | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
解 S* = {B,D,F} 满足精确覆盖:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
4.2 算法X
Donald Knuth提出的算法X是解决精确覆盖问题的递归回溯算法:
- 若矩阵无列,当前部分解即为完整解
- 选择列c(确定性)
- 选择满足A[r,c]=1的行r(非确定性)
- 将行r加入部分解
- 删除所有包含A[i,j]=1的行i和列j
- 递归处理简化后的矩阵
舞蹈链是其高效实现,利用双向链表快速删除/恢复节点。
4.3 转化为精确覆盖
数独需满足4类约束:
- 每行数字不重复
- 每列数字不重复
- 每3×3子网格数字不重复
- 每格仅填一个数字
构建9³×(9×9×4)的矩阵:
private static int BOARD_SIZE = 9;
private static int SUBSECTION_SIZE = 3;
private static int NO_VALUE = 0;
private static int CONSTRAINTS = 4;
private static int MIN_VALUE = 1;
private static int MAX_VALUE = 9;
private static int COVER_START_INDEX = 1;
索引计算方法:
private int getIndex(int row, int column, int num) {
return (row - 1) * BOARD_SIZE * BOARD_SIZE
+ (column - 1) * BOARD_SIZE + (num - 1);
}
矩阵构建方法:
private boolean[][] createExactCoverBoard() {
boolean[][] coverBoard = new boolean
[BOARD_SIZE * BOARD_SIZE * MAX_VALUE]
[BOARD_SIZE * BOARD_SIZE * CONSTRAINTS];
int hBase = 0;
hBase = checkCellConstraint(coverBoard, hBase);
hBase = checkRowConstraint(coverBoard, hBase);
hBase = checkColumnConstraint(coverBoard, hBase);
checkSubsectionConstraint(coverBoard, hBase);
return coverBoard;
}
private int checkSubsectionConstraint(boolean[][] coverBoard, int hBase) {
for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row += SUBSECTION_SIZE) {
for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column += SUBSECTION_SIZE) {
for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
for (int rowDelta = 0; rowDelta < SUBSECTION_SIZE; rowDelta++) {
for (int columnDelta = 0; columnDelta < SUBSECTION_SIZE; columnDelta++) {
int index = getIndex(row + rowDelta, column + columnDelta, n);
coverBoard[index][hBase] = true;
}
}
}
}
}
return hBase;
}
private int checkColumnConstraint(boolean[][] coverBoard, int hBase) {
for (int column = COVER_START_INDEX; column <= BOARD_SIZE; c++) {
for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
int index = getIndex(row, column, n);
coverBoard[index][hBase] = true;
}
}
}
return hBase;
}
private int checkRowConstraint(boolean[][] coverBoard, int hBase) {
for (int row = COVER_START_INDEX; row <= BOARD_SIZE; r++) {
for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++) {
int index = getIndex(row, column, n);
coverBoard[index][hBase] = true;
}
}
}
return hBase;
}
private int checkCellConstraint(boolean[][] coverBoard, int hBase) {
for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++, hBase++) {
for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++) {
int index = getIndex(row, column, n);
coverBoard[index][hBase] = true;
}
}
}
return hBase;
}
初始化棋盘:
private boolean[][] initializeExactCoverBoard(int[][] board) {
boolean[][] coverBoard = createExactCoverBoard();
for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++) {
int n = board[row - 1][column - 1];
if (n != NO_VALUE) {
for (int num = MIN_VALUE; num <= MAX_VALUE; num++) {
if (num != n) {
Arrays.fill(coverBoard[getIndex(row, column, num)], false);
}
}
}
}
}
return coverBoard;
}
4.4 舞蹈节点
舞蹈链的核心是双向链表操作:
class DancingNode {
DancingNode L, R, U, D;
ColumnNode C;
DancingNode hookDown(DancingNode node) {
assert (this.C == node.C);
node.D = this.D;
node.D.U = node;
node.U = this;
this.D = node;
return node;
}
DancingNode hookRight(DancingNode node) {
node.R = this.R;
node.R.L = node;
node.L = this;
this.R = node;
return node;
}
void unlinkLR() {
this.L.R = this.R;
this.R.L = this.L;
}
void relinkLR() {
this.L.R = this.R.L = this;
}
void unlinkUD() {
this.U.D = this.D;
this.D.U = this.U;
}
void relinkUD() {
this.U.D = this.D.U = this;
}
DancingNode() {
L = R = U = D = this;
}
DancingNode(ColumnNode c) {
this();
C = c;
}
}
4.5 列节点
列节点管理列操作:
class ColumnNode extends DancingNode {
int size;
String name;
ColumnNode(String n) {
super();
size = 0;
name = n;
C = this;
}
void cover() {
unlinkLR();
for (DancingNode i = this.D; i != this; i = i.D) {
for (DancingNode j = i.R; j != i; j = j.R) {
j.unlinkUD();
j.C.size--;
}
}
}
void uncover() {
for (DancingNode i = this.U; i != this; i = i.U) {
for (DancingNode j = i.L; j != i; j = j.L) {
j.C.size++;
j.relinkUD();
}
}
relinkLR();
}
}
4.6 求解器
构建舞蹈链网格:
private ColumnNode makeDLXBoard(boolean[][] grid) {
int COLS = grid[0].length;
ColumnNode headerNode = new ColumnNode("header");
List<ColumnNode> columnNodes = new ArrayList<>();
for (int i = 0; i < COLS; i++) {
ColumnNode n = new ColumnNode(Integer.toString(i));
columnNodes.add(n);
headerNode = (ColumnNode) headerNode.hookRight(n);
}
headerNode = headerNode.R.C;
for (boolean[] aGrid : grid) {
DancingNode prev = null;
for (int j = 0; j < COLS; j++) {
if (aGrid[j]) {
ColumnNode col = columnNodes.get(j);
DancingNode newNode = new DancingNode(col);
if (prev == null) prev = newNode;
col.U.hookDown(newNode);
prev = prev.hookRight(newNode);
col.size++;
}
}
}
headerNode.size = COLS;
return headerNode;
}
启发式列选择:
private ColumnNode selectColumnNodeHeuristic() {
int min = Integer.MAX_VALUE;
ColumnNode ret = null;
for (
ColumnNode c = (ColumnNode) header.R;
c != header;
c = (ColumnNode) c.R) {
if (c.size < min) {
min = c.size;
ret = c;
}
}
return ret;
}
递归搜索核心:
private void search(int k) {
if (header.R == header) {
handleSolution(answer);
} else {
ColumnNode c = selectColumnNodeHeuristic();
c.cover();
for (DancingNode r = c.D; r != c; r = r.D) {
answer.add(r);
for (DancingNode j = r.R; j != r; j = j.R) {
j.C.cover();
}
search(k + 1);
r = answer.remove(answer.size() - 1);
c = r.C;
for (DancingNode j = r.L; j != r; j = j.L) {
j.C.uncover();
}
}
c.uncover();
}
}
5. 性能对比
在同一台机器上测试两种算法(避免硬件差异):
- 回溯算法:约250毫秒
- 舞蹈链:约50毫秒
舞蹈链快约5倍!简单粗暴的结论:舞蹈链碾压回溯。
6. 总结
本文用Java实现了两种数独求解算法:
- 回溯算法:简单暴力,适合标准9×9数独
- 舞蹈链:更高效,尤其适合复杂谜题
两种算法都能在秒级解决最难题。完整实现代码可在GitHub获取。