P1784 数独
题目描述
数独是根据 \(9 \times 9\) 盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫内的数字均含 \(1 - 9\) ,不重复。每一道合格的数独谜题都有且仅有唯一答案,推理方法也以此为基础,任何无解或多解的题目都是不合格的。
芬兰一位数学家号称设计出全球最难的“数独游戏”,并刊登在报纸上,让大家去挑战。
这位数学家说,他相信只有“智慧最顶尖”的人才有可能破解这个“数独之谜”。
据介绍,目前数独游戏的难度的等级有一到五级,一是入门等级,五则比较难。不过这位数学家说,他所设计的数独游戏难度等级是十一,可以说是所有数独游戏中,难度最高的等级。他还表示,他目前还没遇到解不出来的数独游戏,因此他认为“最具挑战性”的数独游戏并没有出现。
输入格式
一个未填的数独。
输出格式
填好的数独。
输入输出样例 #1
输入 #1
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
输出 #1
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
说明/提示
2022-04-17 @farteryhr 贡献了三组 hack 数据。加入了其中两组。第三组过强(来源:https://www.dcc.fc.up.pt/~acm/sudoku.pdf),放在下边供自测。
9 0 0 8 0 0 0 0 0
0 0 0 0 0 0 5 0 0
0 0 0 0 0 0 0 0 0
0 2 0 0 1 0 0 0 3
0 1 0 0 0 0 0 6 0
0 0 0 4 0 0 0 7 0
7 0 8 6 0 0 0 0 0
0 0 0 0 3 0 1 0 0
4 0 0 0 0 0 2 0 0
输出
9 7 2 8 5 3 6 1 4
1 4 6 2 7 9 5 3 8
5 8 3 1 4 6 7 2 9
6 2 4 7 1 8 9 5 3
8 1 7 3 9 5 4 6 2
3 5 9 4 6 2 8 7 1
7 9 8 6 2 1 3 4 5
2 6 5 9 3 4 1 8 7
4 3 1 5 8 7 2 9 6
Tip
📌 题目分析
给定数据:
要求推理出所有剩余空格的数字,并输出唯一解。
👉 本质:
👉 类型:
🧠 解题思路
1️⃣ 为什么是回溯法
数独是回溯算法的“教科书级”应用。 由于数独有极其严格的规则限制,当我们在一个空格填入数字时,就像是在走迷宫时选择了一个岔路口。 * 如果这个数字填对了,我们就继续往下填下一个空格。 * 如果填到后面发现“死胡同”了(某个空格 1 到 9 全都填不进去),说明我们前面肯定有哪一步选错了。此时就需要退回来(回溯),把之前的数字擦掉,换一个数字重新试。
2️⃣ 核心技巧:O(1) 的快速校验
如果我们每次填数字都要去用 for 循环检查一遍这一行、这一列、这一个九宫格有没有重复数字,程序会跑得很慢。
空间换时间技巧:我们开三个二维布尔数组来当“登记册”:
* row[x][i]:表示第 x 行是否已经有了数字 i。
* col[y][i]:表示第 y 列是否已经有了数字 i。
* block[b][i]:表示第 b 个九宫格是否已经有了数字 i。
这样,校验某个数字能不能填,只需要 if (!row[x][i] && !col[y][i] && !block[b][i]) 这一瞬间的判断即可。
3️⃣ 九宫格编号的数学魔法
对于坐标 (x, y)(假设下标从 1 开始到 9),它到底属于哪个 \(3 \times 3\) 的九宫格呢?
这是一个极其经典的坐标映射公式。我们将 9×9 分割为 9 个区块,编号 1 到 9,公式如下:
4️⃣ 时间复杂度
虽然最坏情况下 DFS 的时间复杂度是 \(O(9^{\text{空白格子数}})\),但在数独极其严苛的剪枝条件(三重校验)下,绝大多数错误分支会在第一两步就被立刻扼杀。对于 \(9 \times 9\) 的标准数独,这种朴素的 DFS 回溯法能在几毫秒内瞬间秒杀。
💻 C++代码
#include <iostream>
using namespace std;
int a[15][15]; // 数独棋盘
bool row[10][10]; // row[x][i] 记录第 x 行是否用过数字 i
bool col[10][10]; // col[y][i] 记录第 y 列是否用过数字 i
bool block[10][10]; // block[b][i] 记录第 b 个宫是否用过数字 i
// 根据横纵坐标计算属于第几个 3x3 宫格
int get_block(int x, int y) {
return (x - 1) / 3 * 3 + (y - 1) / 3 + 1;
}
// 打印最终答案
void print_ans() {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= 9; j++) {
cout << a[i][j] << " ";
}
cout << "\n";
}
}
// DFS 回溯填数,返回 bool 代表是否找到了最终解
bool dfs(int x, int y) {
// 1. 如果 x 走到了 10,说明 1~9 行全部填完,成功!
if (x == 10) {
print_ans();
return true;
}
// 2. 如果当前行走到了尽头(y 到了 10),换到下一行的第 1 列继续搜
if (y == 10) {
return dfs(x + 1, 1);
}
// 3. 如果当前格子已经有数字了,直接跳过,搜下一个格子
if (a[x][y] != 0) {
return dfs(x, y + 1);
}
// 4. 当前格子是 0,尝试填入 1~9
int b = get_block(x, y);
for (int i = 1; i <= 9; i++) {
// 校验:行、列、宫中都没有用过该数字
if (!row[x][i] && !col[y][i] && !block[b][i]) {
// 【保护现场】填入数字,并登记
a[x][y] = i;
row[x][i] = true;
col[y][i] = true;
block[b][i] = true;
// 继续往下搜,如果底层的搜索返回 true,说明整个数独解完了,一路向上传递结束信号
if (dfs(x, y + 1)) {
return true;
}
// 【恢复现场 / 回溯】底层的路走不通,撤销当前的操作,换下一个数字试
a[x][y] = 0;
row[x][i] = false;
col[y][i] = false;
block[b][i] = false;
}
}
// 1~9 都试了还是不行,说明上一步填错了,返回 false 让上一步回溯
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= 9; j++) {
cin >> a[i][j];
if (a[i][j] != 0) {
int val = a[i][j];
row[i][val] = true;
col[j][val] = true;
block[get_block(i, j)][val] = true;
}
}
}
// 从坐标 (1, 1) 开始搜索
dfs(1, 1);
return 0;
}
⚠️ 易错点
❌ 1. 返回值设计缺陷导致输出乱码
很多人在写 dfs(x, y) 时习惯用 void 类型。当走到 x == 10 时打印数组然后 return;。
致命失误:数独只需要输出一个正确解。如果不用 bool 类型层层向上阻断,程序在输出正确答案后,会继续往后回溯,又把正确的数字擦掉去尝试别的组合,甚至输出多个错乱的棋盘。利用 if(dfs(...)) return true; 这一招可以实现“找到就立刻强制全剧终”的效果。
❌ 2. 忘记初始化原始数据
读取棋盘数据时,对于非 0 的已知数字,必须立刻在对应的 row, col, block 数组中打上 true 的标记! 如果漏了这一步,系统会认为 1~9 都还没填过,导致原有的数字在同一行被重复填入。
❌ 3. 宫格数组索引越界或重叠
如果你不用 1 到 9,而是混合使用 0 到 8 来计算坐标,特别容易在 b = (x/3)*3 + (y/3) 这种地方搞错。统一让所有的坐标、数字、宫格都保持在 1 ~ 9 的闭区间,是避免下标越界和调试痛苦的最佳实践。
🚀 一句话总结
🔥 补充:硬核知识点
进阶:Dancing Links (舞蹈链 / DLX) 算法
虽然普通的 DFS 回溯可以秒杀 \(9 \times 9\) 数独,但如果你面对的是变态级的 \(16 \times 16\) 甚至 \(25 \times 25\) 的超大数独(例如洛谷上的高阶靶场题),普通 DFS 会直接超时到天荒地老。
这时候,计算机科学巨擘、图灵奖得主 Donald Knuth(高德纳) 提出了一个极其优美的数据结构:十字交叉双向循环链表 (Dancing Links)。
- 降维打击原理:DLX 将数独问题巧妙地转换为了图论中经典的“精确覆盖问题 (Exact Cover)”。它把行、列、宫、格的约束转化为一个极大的稀疏 01 矩阵,每一行代表一个“在 (x,y) 填入数字 v”的动作,每一列代表一个约束。
- 舞蹈般的回溯:在回溯时,利用链表删除和恢复节点只需改变一下前后指针的指向,时间复杂度为绝对的 \(O(1)\):
L[R[c]] = c; R[L[c]] = c;指针解开又缝合的过程就像在跳舞一样优雅,因此被命名为“舞蹈链”。它是目前人类已知的解决数独及各类拼图游戏最快、最强悍的终极算法!