
根据百度百科的描述:数独(shù dú)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个九宫格内的数字均含1-9,不重复。
-
下面给出数独问题的不同解法,从最简单的暴力开始,然后使用二进制进行优化,最后采用Dancing links进行优化。
-
下面给出三个题目:
-
第一个题目使用最暴力的解法;
-
第二个题目使用二进制优化;
-
最后一个题目使用两种优化方式求解,一种是二进制优化,另一种是Dancing links。
-
问题描述
-
问题链接:AcWing 1613. 数独简单版
分析
-
可以开辟三个判重数组,分别存储行、列、九宫格已经有哪些数字被填写了。这里九宫格从左向右,从上向下标号依次是(0, 0)~(2, 2),对于某个坐标(i, j),其所在的九宫格编号为(i/3, j/3)。
-
暴搜所有方案,然后判断是否合法即可。从左上角向右下角搜索,一个格子一个格子的考虑,每次考虑完一行,跳到下一行即可。
代码
- C++
#includeAcWing 166. 数独using namespace std; const int N = 10; char g[N][N]; bool row[N][N], col[N][N], cell[3][3][N]; bool dfs(int x, int y) { if (y == 9) return dfs(x + 1, 0); if (x == 9) { for (int i = 0; i < 9; i++) puts(g[i]); return true; } if (g[x][y] != '.') return dfs(x, y + 1); for (int i = 1; i <= 9; i++) if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i]) { row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true; g[x][y] = i + '0'; if (dfs(x, y + 1)) return true; g[x][y] = '.'; row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false; } return false; } int main() { for (int i = 0; i < 9; i++) { scanf("%s", g[i]); for (int j = 0; j < 9; j++) if (g[i][j] != '.') { int x = g[i][j] - '0'; row[i][x] = col[j][x] = cell[i / 3][j / 3][x] = true; } } dfs(0, 0); return 0; }
问题描述
-
问题链接:AcWing 166. 数独
分析
-
我们可以依次填写每个空格,枚举所有情况即可。
-
考虑如何剪枝:
(1)优化搜索顺序:我们应该分支较少的格子,即可填数字较少的格子。
(2)排除等效冗余:无
(3)可行性剪枝:当前空位中所填写的内容不能与所在行、列、九宫格有重复数组。
(4)最优性剪枝:无。
-
另外我们还可以使用位运算优化:
- 我们可以使用一个9位的二进制数据表示某一行或者某一列或者某个九宫格的状态,二进制位是0代表可以该行中已经有该数据了,比如0b110100000代表该行(或者该列、或者该九宫格)还可以填写9或者8或者6。
-
求解二进制中最右侧的1可以使用lowbit *** 作。
代码
- C++
#includeAcWing 169. 数独2using namespace std; const int N = 9, M = 1 << N; int ones[M], map[M]; // ones[i]表示i二进制表示中1的个数; map[i]表示log2(i) int row[N], col[N], cell[3][3]; // 二进制优化 char str[100]; // 将row, col, cell二进制标识全部置为1,代表可以放置数字 void init() { for (int i = 0; i < N; i++) row[i] = col[i] = (1 << N) - 1; for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) cell[i][j] = (1 << N) - 1; } // is_set = true的话,在(x, y)放置上数字t,否则取消放置 void draw(int x, int y, int t, bool is_set) { if (is_set) str[x * N + y] = '1' + t; else str[x * N + y] = '.'; int v = 1 << t; if (!is_set) v = -v; row[x] -= v; col[y] -= v; cell[x / 3][y / 3] -= v; } int lowbit(int x) { return x & -x; } // 获取(x, y)位置可以放置的数据 int get(int x, int y) { return row[x] & col[y] & cell[x / 3][y / 3]; } // cnt: 目前剩余需要填写位置的数量 bool dfs(int cnt) { if (!cnt) 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 * N + j] == '.') { int state = get(i, j); if (ones[state] < minv) { minv = ones[state]; x = i, y = j; } } int state = get(x, y); for (int i = state; i; i -= lowbit(i)) { int t = map[lowbit(i)]; draw(x, y, t, true); if (dfs(cnt - 1)) return true; draw(x, y, t, false); } return false; } int main() { for (int i = 0; i < N; i++) map[1 << i] = i; for (int i = 0; i < 1 << N; i++) for (int j = 0; j < N; j++) ones[i] += i >> j & 1; while (cin >> str, str[0] != 'e') { 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'; draw(i, j, t, true); } else cnt++; dfs(cnt); puts(str); } return 0; }
问题描述
-
问题链接:AcWing 169. 数独2
分析
- 存在两种解法:
解法一
-
搜索顺序:每次找到一个空格,枚举空格选择哪个字母,然后往下递归。当所有空格都被填完,说明找到一个合法答案。
-
优化:
-
(1)对于每个空格:如果不能填写任何一个字母,无解;如果只能填写一个字母,那么直接填上;
-
(2)对于每一行:如果某个字母不能出现在任何位置,无解;如果某个字母只有一个位置可以填写,则直接填上;
-
(3)对于每一列,和行类似;
-
(4)对于每个16宫格,和行类似;
-
(5)每次选择空格填写时,选择被选方案最少的格子来填。
-
解法二
-
使用Dancinglinks求解,需要将问题转化为精确覆盖问题。
-
首先需要将该问题转化为一个01矩阵,行代表选择,列代表限制。
-
这里的行表示:每个格子选择哪个数(用0~15表示A~P)。一共 1 6 2 16 ^ 2 162 个格子,每个格子16种选择,因此需要 1 6 3 16 ^ 3 163 行。
-
列表示:
-
(1)每个格子恰好填一个数;占用 1 1 1 ~ 1 6 2 16^2 162 列;
-
(2)每行0~15恰好出现一次;占用 1 6 2 + 1 16^2+1 162+1 ~ 1 6 2 × 2 16^2 times 2 162×2 列;
-
(3)每列0~15恰好出现一次;占用 1 6 2 × 2 + 1 16^2 times 2+1 162×2+1 ~ 1 6 2 × 3 16^2 times 3 162×3 列;
-
(4)每个方格0~15恰好出现一次;占用 1 6 2 × 3 + 1 16^2 times 3+1 162×3+1 ~ 1 6 2 × 4 16^2 times 4 162×4 列;
-
-
对于第(4)个限制,我们从左到右,从上到下给每个方格的编号为0~15。
-
每一行我们只需要填写四个1,因此总共1的数量为 1 6 3 × 4 16 ^ 3 times 4 163×4,这里的节点数目上限N=20000即可。
代码
- C++
// 解法一:加入各种优化 #include#include #include using namespace std; const int N = 16, M = N * N + 1; int ones[1 << N]; // 存储每个数的二进制表示中有多少个1,例如ones[5] = 2 int log[1 << N]; // 存储数据的log值, log[1<> 1] + (i & 1); // 使用下面的方法求解ones也是可以的 // for (int i = 0; i < 1 << N; i++) { // for (int j = i; j; j -= lowbit(j)) ones[i]++; // } // 多组测试数据 while (cin >> str[0]) { for (int i = 1; i < N; i++) cin >> str[i]; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) state[i][j] = (1 << N) - 1; } int cnt = 0; // 存储需要填写的空格的个数 for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) if (str[i][j] != '-') draw(i, j, str[i][j] - 'A'); else cnt++; } dfs(cnt); for (int i = 0; i < N; i++) cout << str[i] << endl; cout << endl; } return 0; }
// 解法二:Dancing links #include#include #include using namespace std; const int N = 20000; int m = 16 * 16 * 4; // 列数 int u[N], d[N], l[N], r[N], s[N], col[N], row[N], idx; int ans[N], top; // 存储选的行(行代表 *** 作) struct Op { int x, y; char z; } op[N]; // 存储每行代表的 *** 作 char g[N][N]; // 存储数独 void init() { for (int i = 0; i <= m; i++) { l[i] = i - 1, r[i] = i + 1; s[i] = 0; d[i] = u[i] = i; } l[0] = m, r[m] = 0; idx = m + 1; } void add(int &hh, int &tt, int x, int y) { row[idx] = x, col[idx] = y, s[y]++; u[idx] = y, d[idx] = d[y], u[d[y]] = idx, d[y] = idx; r[hh] = l[tt] = idx, r[idx] = tt, l[idx] = hh; tt = idx++; } void remove(int p) { r[l[p]] = r[p], l[r[p]] = l[p]; for (int i = d[p]; i != p; i = d[i]) for (int j = r[i]; j != i; j = r[j]) { s[col[j]]--; u[d[j]] = u[j], d[u[j]] = d[j]; } } void resume(int p) { for (int i = u[p]; i != p; i = u[i]) for (int j = l[i]; j != i; j = l[j]) { u[d[j]] = j, d[u[j]] = j; s[col[j]]++; } r[l[p]] = p, l[r[p]] = p; } bool dfs() { if (!r[0]) return true; int p = r[0]; for (int i = r[0]; i; i = r[i]) if (s[i] < s[p]) p = i; if (!s[p]) return false; remove(p); for (int i = d[p]; i != p; i = d[i]) { ans[++top] = row[i]; for (int j = r[i]; j != i; j = r[j]) remove(col[j]); if (dfs()) return true; for (int j = l[i]; j != i; j = l[j]) resume(col[j]); top--; } resume(p); return false; } int main() { while (~scanf("%s", g[0])) { for (int i = 1; i < 16; i++) scanf("%s", g[i]); init(); // 初始化DLX for (int i = 0, n = 1; i < 16; i++) // n表示当前添加的行 for (int j = 0; j < 16; j++) { int a = 0, b = 15; if (g[i][j] != '-') a = b = g[i][j] - 'A'; for (int k = a; k <= b; k++, n++) { int hh = idx, tt = idx; op[n] = {i, j, k + 'A'}; // 记录操作 // (1) (i, j)这个格子只能填一个数 add(hh, tt, n, i * 16 + j + 1); // (2) 每行中0~15恰好出现一次(第i行填写数字k) add(hh, tt, n, 256 + i * 16 + k + 1); // (3) 每列中0~15恰好出现一次(第j列填写数字k) add(hh, tt, n, 256 * 2 + j * 16 + k + 1); // (4) 每个方格0~15恰好出现一次(第i/4*4+j/4个方格填写数字k) add(hh, tt, n, 256 * 3 + (i / 4 * 4 + j / 4) * 16 + k + 1); } } dfs(); // 求解答案 for (int i = 1; i <= top; i++) { auto t = op[ans[i]]; g[t.x][t.y] = t.z; } for (int i = 0; i < 16; i++) puts(g[i]); puts(""); } return 0; }
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)