[POJ2676]Sudoku
这个题 题意很明显 就是个求数独的题目。
- 常规做法:暴力枚举+判断可行性(会TLE)
- 进阶做法:一边算一边用数组标记(400多ms)
- 超神做法:用DLX算法(基本0ms)
这三种做法我都试过 一步步优化程度提升 最后DLX特别快 基本算普通的不需要时间 算骨灰级难度的也很快
所以我在这里介绍的就是DLX算法了 一开始不太明白 看了个大佬的程序 就基本上懂了 十分简洁明了 博客链接
但那个大佬的代码没啥注释 我就将她的代码进行略微改编,然后增加注释
我还是简单介绍下DLX算法吧(来自《算法竞赛入门经典-训练指南》):
这个算法是用于解决一些精准覆盖的问题 即有几个集合中包含一些元素 选取一些集合 使每个元素不重复且恰好出现一次。(可以运用到,N皇后,数独即一些特殊的问题上面去)
需要用一个01行列的表构造(以后有时间补上书上的说明)
很容易想得出是用搜索写法。 这里就是运用的搜索中的X算法: 就是选取还没有选过的一个元素,从包含它的元素集合中挑选,然后删除其他所有包含这个元素的集合。最后回溯时又复原这个集合。
为了更快的删除和恢复。所以我们就需要一个特殊的数据结构来维护,这里就出现了神奇的Dancing-Links(舞蹈链)。
这个数据结构记录了它上下左右最近的一个为1的存在节点,这个是运用的循环链表实现的,能够达到很快遍历该行和该列的目的。而且恢复也十分容易,只要修改相邻的节点就行了。 由于是循环列表,所以最后一列右边的节点就是第一列的节点,其他三个方面类推。
这个数独是怎么运用的后面去补吧。。。 最重要的是要分清本身的棋盘和舞蹈链组成的舞池的区别(因为都是矩阵 一开始不好区分)
#include <cstdio> #include <cstring> #include <cstdlib> #include <iostream> #include <cmath> #include <algorithm> #define Set(a, v) memset(a, v, sizeof(a)) #define For(i, l, r) for(int i = (l); i <= (int)(r); ++i) #define Fordown(i, r, l) for(int i = (r); i >= (int)(l); --i) #define Travel(i, A, s) for(int i = A[s]; i != s; i = A[i]) //之所以可以这样遍历 因为 这是个循环队列 可以遍历一列或者一行 using namespace std; inline int read(){ int x = 0, fh = 1; char ch; for(; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1; for(; isdigit(ch); ch = getchar()) x = (x<<1) + (x<<3) + (ch^'0'); return x * fh; } const int maxr = 9*9*9 + 10; const int maxc = 9*9*4 + 10; const int inf = 0x3f3f3f3f; int L[maxc+maxr*5], R[maxc+maxr*5], U[maxc+maxr*5], D[maxc+maxr*5]; //maxc+maxr*5是总共的最大节点数 //这边分别是四个指针,left,right,up,down上下左右 分别指向别的节点位置 int S[maxc]; //统计每列1的节点个数 int nRow[maxc+maxr*5], nCol[maxc+maxr*5]; //储存每个节点的行和列 int Head[10][10][10]; //储存每个位置 (数独中) 每个数字所对应的节点 int cnt = 0; //存储共有多少个节点 int G[10][10]; //存储答案图(和原来的图) inline int sub_grid (int x, int y) {return ((x-1)/3)*3+((y-1)/3+1);} //把每个原数独中的节点所在宫的编号求出来 void insert (int c, int cnt) { U[D[c]] = cnt; D[cnt] = D[c]; U[cnt] = c; D[c] = cnt; ++S[c]; nCol[cnt] = c; } //插入一个节点 void remove (int c) { L[R[c]] = L[c]; //右边的左边 变为这个节点的左边 R[L[c]] = R[c]; //右边的左边 变为这个节点的左边 //即删除这一列 Travel (i, D, c) //向下遍历 Travel (j, R, i) { //向右遍历 U[D[j]] = U[j]; //楼下的楼上变为楼上 D[U[j]] = D[j]; //楼下的楼上变为楼上 --S[nCol[j]]; //减少遍历行中含有1的列的所统计个数 } //删除包含这列有1的行 } //删除一列 void resume (int c) { Travel (i, U, c) Travel (j, L, i) { U[D[j]] = D[U[j]] = j; ++S[nCol[j]]; } L[R[c]] = R[L[c]] = c; } //恢复一列 bool dfs (int d) { if (d > 81) return true; int c, minn = inf; Travel (i, R, 0) { if (!S[i]) return false; if (S[i] < minn) { minn = S[i]; c = i; } } remove(c); Travel(i, D, c) { int tmp = nRow[i]; G[tmp/100][(tmp/10)%10] = tmp % 10; Travel (j, R, i) remove (nCol[j]); if (dfs(d+1)) return true; Travel (j, L, i) resume(nCol[j]); } resume(c); return false; } void init() { For (i, 0, 81*4) { S[i] = 0; //把每列的个数清零 U[i] = D[i] = i; //这个节点上下变为自己 L[i] = i-1; R[i] = i+1; nCol[i] = 0; // } R[81*4] = 0; L[0] = 81*4; cnt = 81*4; For (i, 1, 9) For (j, 1, 9) { if (G[i][j]) { //有数字的情况 int k = G[i][j]; //取出这一位 For (u, 1, 4) { L[cnt+u] = cnt + u - 1; // R[cnt+u] = cnt + u + 1; // nRow[cnt+u] = 100*i + 10*j + k; //把这行标号 } L[cnt+1] = cnt+4; R[cnt+4] = cnt+1; Head[i][j][k] = cnt + 1; insert ((i-1) * 9 + j, cnt+1); insert (81 + (i-1) * 9 + k, cnt+2); insert (81*2 + (j-1) * 9 + k, cnt+3); insert (81*3 + (sub_grid(i, j) - 1) * 9 + k, cnt+4); cnt += 4; } else { For (k, 1, 9) { For (u, 1, 4) { L[cnt+u] = cnt + u - 1; R[cnt+u] = cnt + u + 1; nRow[cnt+u] = 100*i + 10*j + k; } L[cnt+1] = cnt+4; R[cnt+4] = cnt+1; Head[i][j][k] = cnt + 1; insert ((i-1) * 9 + j, cnt+1); insert (81 + (i-1) * 9 + k, cnt+2); insert (81*2 + (j-1) * 9 + k, cnt+3); insert (81*3 + (sub_grid(i, j) - 1) * 9 + k, cnt+4); cnt += 4; } } } } void solve () { int k = 0; For (i, 1, 9) For (j, 1, 9) if (G[i][j]) { ++k; int v = Head[i][j][G[i][j]]; remove (nCol[v] ); Travel(u, R, v) remove (nCol[u]); } dfs(k+1); For (i, 1, 9) { For (j, 1, 9) putchar (G[i][j] + '0'); putchar ('\n'); } // putchar ('\n'); } void input() { Set(G, 0); int cnt_x = 1, cnt_y = 0; while (cnt_x != 9 || cnt_y != 9) { char ch = getchar(); if (!isdigit(ch)) continue; //判断是否为数字 (特殊的读入方式) ++cnt_y; if (cnt_y == 10) { cnt_y = 1; ++cnt_x; //到这行结束 跳到下一行 } G[cnt_x][cnt_y] = (ch ^ '0'); //把当前这个数独的位置记下来 } } int main (){ int t = read(); while (t--) { input(); //输入 init(); //预处理 solve(); //解决问题 } }