AcWing 166 Sudoku(9*9)
AcWing 166 Sudoku(9 * 9)
题目描述:
数独是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得图中每行、每列、每个 3×3 的九宫格内数字 1∼9均恰好出现一次。请编写一个程序填写数独。
输入样例
4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end
输出样例
417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936
题目分析
题目明显要使用DFS搜索,只需要对每个空格位置依次选择可以放入的数字,直到遍历完整张图为止,然而,数度问题的搜索树十分庞大,直接盲目搜索会导致超时,此时需要引入一些优化手段。
-
如果让你来完成这个数独,你会从那里开始入手?
很明显,我们一定会选择可以填入数字数量最少的格子开始枚举,这样更有可能得到最优解,而不是不停失败回溯。因此,我们的程序也可以从这个方向入手。
-
在搜索程序中,影响时间效率的因素除了搜索树的规模(时间复杂度)之外,还有在每个状态结点上的纪律、检索、更新的开销(常数)。我们可以使用位运算来代替数组执行“对数独各个位置”所填数字的纪律和检查与统计,这就是所说的”常数优化“。具体方案:
- 对于每行、每列、每个九宫格,用一个九位二进制数(全局整数变量)保存哪些数字可以被填(1表示当前数字可填,反之不可)。
- 如何求出某个位置有那些数字可填?只需要对行、列、九宫格作交集运算即可(&运算)。
- 如何快速取出每个可选的数字?用lowbit运算,再用数组存储对应二进制数所代表的数字,可以用O(1)的时间取出每个可选数字。
- 在某个位置填入一个数字后,只需要把该位置对应的行、列、九宫格记录的二进制位改为0即可更新状态,回溯只需要改为1还原现场。
- 采用二进制优化后,每个状态可选数字数量就是二进制中1的数量,我们可以设一个ones数组记录每个数字二进制中1的数量,就可以用O(1)的时间取出该数量。
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 9;
int ones[1 << N], map[1 << N]; // ones记录每个数字二进制中有多少个1,map记录每个二进制下的1对应的数字
int row[N], line[N], cell[3][3]; // 用二进制来存储每行/列/九宫格的可选状态
char str[100];
inline int lowbit (int x) { return x & (-x); }
void init () {
// 一开始,所有状态都为'.',所有数都可以选择
for (int i = 0; i < N; i++) row[i] = line[i] = (1 << N) - 1;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
cell[i][j] = (1 << N) - 1;
}
// 求出(x, y)位置的可选状态
inline int get (int x, int y) {
return row[x] & line[y] & cell[x / 3][y / 3];
}
bool dfs (int cnt) {
if (cnt == 0) return true;
// 找出可选方案数最小的格子
int minv = 10;
int x, y;
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
if (str[i * 9 + j] == '.') {
int t = ones[get(i, j)]; // 可选方案的交集
if (t < minv) {
minv = t;
x = i, y = j;
}
}
for (int i = get(x, y); i; i -= lowbit(i)) {
int t = map[lowbit(i)];
row[x] -= 1 << t;
line[y] -= 1 << t;
cell[x / 3][y / 3] -= 1 << t;
str[x * 9 + y] = '1' + t;
if (dfs(cnt - 1)) return true;
row[x] += 1 << t;
line[y] += 1 << t;
cell[x / 3][y / 3] += 1 << t;
str[x * 9 + y] = '.';
}
return false;
}
int main () {
// 预处理1
for (int i = 0; i < 1 << N; i++) {
int s = 0;
for (int j = i; j; j -= lowbit(j)) s++;
ones[i] = s;
}
// 预处理2
for (int i = 0; i < N; i++) map[1 << i] = i;
while (cin >> str, str[0] != 'e') {
// 预处理3
init ();
// 根据实际给定的数据,更新每行/列/九宫格的可选状态集
int cnt = 0; // 需要填入的空格
for (int i = 0, k = 0; i < N; i++) {
for (int j = 0; j < N; j++, k++) {
if (str[k] != '.') {
int t = str[k] - '1'; // 把1~9印射成0~8(方便位运算)
row[i] -= 1 << t;
line[j] -= 1 << t;
cell[i / 3][j / 3] -= 1 << t;
}
else cnt++;
}
}
// 对剩下需要填入的cnt个格子搜索
dfs(cnt);
cout << str << endl;
}
return 0;
}