舞蹈链(Dance Link X)算法生成数独和解数独 C语言实现
1. Dancing Links介绍 先看完下面这个文章的精确覆盖问题讲解
2. X 算法 通过上述步骤,可将 X 算法的流程概括如下:
对于现在的矩阵 M ,选择并标记一行 r,将 r 添加至 S(答案集合) 中;
如果尝试了所有的 r 却无解,则算法结束,输出无解;
标记与 r 相关的行 ri 和列 ci
删除所有标记的行和列,得到新矩阵 M’;
如果 M’ 为空,且 r 全为1 ,则算法结束,输出被删除的行组成的集合S; 如果 M’ 为空,且 r 不全为1,则恢复与 r 相关的行 ri 以及列 ci,跳转至步骤 1; 如果 M’ 不为空,则跳转至步骤 1。
不难看出,X 算法需要大量的「删除行」、「删除列」和「恢复行」、「恢复列」的操作。
一个朴素的想法是,使用一个二维数组存放矩阵,再用四个数组分别存放每一行与之相邻的行编号,每次删除和恢复仅需更新四个数组中的元素。但由于一般问题的矩阵中 0 的数量远多于 1 的数量,这样做的空间复杂度难以接受。
Donald E. Knuth 想到了用双向十字链表来维护这些操作。
而在双向十字链表上不断跳跃的过程被形象地比喻成「跳跃」,因此被用来优化 X 算法的双向十字链表也被称为「Dancing Links」。
3. Dancing Links X 过程
3.1 图1 构建的十字循环链表如图1所示,其中columns数组是辅助节点,方便快速定位到相关列,其代码结构如下
辅助数组 1 2 3 4 5 6 7 8 9 typedef struct ColumnNode { struct Node *head ; int size; int col; } ColumnNode; ColumnNode *columns[4 *SIZE*SIZE];
ColumnNode结构中的head指针指向该列的哨兵节点(十字循环链表的第一行所有节点都是哨兵节点,便于Dance),head的代码结构如下:
DLX节点 1 2 3 4 5 6 typedef struct Node { struct Node *left , *right , *up , *down ; struct ColumnNode *column ; int row, col; } Node;
Node结构中的*left, *right, *up, *down;分别指向其上下左右的节点
Node结构中*column 指向该列对应的辅助节点
Node结构row, col;记录在十字链表中的行和列
图1中的root节点,head节点,还有实际的数字节点,其代码结构都是Node,只是用途不一样
3.2 图2 图2展示的是实际的十字链表的删除列(实际上是半删除)的操作,和[2.X 算法]中使用矩阵演示删除列的方式不完全一样。
图2中演示的是删除第0列以及和他相关的行,
删除第0列时,只需要半删除第0列下的head节点
(半删除的节点为图2中灰色节点),而第0列下其他节点(节点8)
不动。为什么说是半删除,因为head节点的left和right依然指向删除之前的节点(图2中虚线),只是root节点和下一个head相连了(图2中红色线),半删除还有一个重要作用就是在回溯时,能够快速的恢复十字循环链表。
然后再删除第0列相关联的所有行,即图2中的第4行(最后一行),包含节点9和节点10(节点8依然不动
),节点9和节点10的up和down指针依然指向删除之前的节点(图2中虚线)
这样一直个完整删除列就做完了,对应代码如下:
删除列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void removeCol (ColumnNode *columnNode) { Node *head = columnNode->head; head->right->left = head->left; head->left->right = head->right; for (Node *row = head->down; row != head; row = row->down) { for (Node *j = row->right; j != row; j = j->right) { j->down->up = j->up; j->up->down = j->down; columns[j->col]->size--; } } }
3.3 图3 图3 是删除第0列后,隐藏半删除的等无关节点,剩余的十字循环链表的样子
4. 数独生成 生成过程如下:
0、求解子程序(精确覆盖)。这个子程序可用dancing links算法,不是逻辑解法。可以在非唯一解的情况下找到任意一个解。
answers[dep]数组 存放每次递归选择的行的行号,最终这些行即为满足所以约束的答案
dancinglinks算法 递归+回溯 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 bool dance (int dep) { if (root->right == root) return true ; for (Node *node = root->right; node != root; node = node->right) { if (node->down == node) return false ; } int col = root->right->col; for (Node *node = root->right; node != root; node = node->right) { if (node->column->size < columns[col]->size) col = node->column->col; } ColumnNode *columnNode = columns[col]; removeCol(columnNode); for (Node *row = columnNode->head->down; row != columnNode->head; row = row->down) { answers[dep] = row->row; for (Node *j = row->right; j != row; j = j->right) removeCol(columns[j->col]); if (dance(dep + 1 )) return true ; for (Node *j = row->left; j != row; j = j->left) restoreCol(columns[j->col]); } restoreCol(columnNode); return false ; }
1、随机放入11个数,用0中的子程序求任意解生成终盘。(相关研究表明,放入11个数,有解的概率约99.7%,12个数有解概率降低不少,10个数虽然概率更高,但求解时间增长。故11个合适。)
initConstraint[] 数组用来保证随机生成的数的所有约束不重复
设定 随机生成的数字和 [i,j,k] 对应,i:数独中的第几行,j数独中的第几类,k填入的数字1-9
这里的a,b,c,d为 数字[i,j,k] 的4个约束所在的位置,计算逻辑如下:
首先,这个格子里必须有数字,即不能为空
其次,同一行不能有相同数字
再次,同一列不能有相同数字
最后,同一个九宫格不能有相同数字
每一个数字有4个约束条件,我们需要填81个数字,所以一共是324个约束条件,所以,将数独转换为精确覆盖的问题就是如何将这324个约束条件以互相独立的形式给表现出来,如下:
324个约束条件对应324个列(所有的都坐标,下标都是从0开始)
第0列表示数独(0,0)位置是否有数字
第1列表示(0,1)位置是否有数字
……
第8列表示(0,8)是否有数字
第9列表示(1,0)是否有数字
……
第80列表示(8,8)是否有数字
以上81列表示数独的格子里是否有数字的约束条件,接着
81列表示第0行是否有数字1
82列表示第0行是否有数字2
……
89列表示第0行是否有数字9
90列表示第1行是否有数字1
91列表示第1行是否有数字2
……
161列表示第8行是否有数字9
以上为行的约束条件
162列表示第0列是否有数字1
……
242列表示第8列是否有数字9
最后,是九宫格的约束条件:
243列表示第0个九宫格是否有数字1
244列表示第0个九宫格是否有数字2
……
323列表示第8个九宫格是否有数字9
以上就完成了将数独转换成精确覆盖问题。每在一个格子上填上一个数字就占有上述的4个列,填上81个符合要求的数字正好将324个列精确覆盖,下面来看如何根据要填的数字得到相应的列
这里用(i,j,k)表示要在第i行、第j列的空格上填上数字k,例如(0,4,9)表示在第0行、第4列填上数字9
根据这3个数字,可以算出相应的4个约束列(用abcd表示)分别为:
a = i * 9 + j b = i * 9 + k + 80 c = j * 9 + k + 161 d = (i / 3 * 3 + j / 3) * 9 + k + 242
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 int initConstraint[4 *SIZE*SIZE]={0 };void initLocationDict (int initCount) { srand(time(NULL )); for (int num = 0 ; num < initCount; num++) { while (true ) { printf ("随机生成数字\n" ); int i = rand() % SIZE; int j = rand() % SIZE; int k = rand() % SIZE + 1 ; printf ("num=%d,i=%d,j=%d,k=%d\n" , num, i, j, k); int a = i * 9 + j; printf ("%d\n" , initConstraint[a] ); if (initConstraint[a] == 1 ) continue ; int b = i * 9 + k + 80 ; printf ("%d\n" , initConstraint[b] ); if (initConstraint[b] == 1 ) continue ; int c = j * 9 + k + 161 ; printf ("%d\n" , initConstraint[c] ); if (initConstraint[c] == 1 ) continue ; int d = (i / 3 * 3 + j / 3 ) * 9 + k + 242 ; printf ("%d\n" , initConstraint[d] ); if (initConstraint[d] == 1 ) continue ; initConstraint[a] = 1 ; initConstraint[b] = 1 ; initConstraint[c] = 1 ; initConstraint[d] = 1 ; sudoku[i][j] = k; printf ("随机生成第%d个数字%d,i=%d,j=%d \n" , num, k, i, j); break ; } } }
2、生成终盘后,不断随机挖掉一个数。每次挖掉一个数,检查是否存在唯一解。方法:如果挖掉一个1,在该方格中依次填入2~9,调用0中的子程序求解。
暂未实现
3、判断难度。比较简单的方法是,计算每一个空格有多少个候选数,总候选数越多,可能越难。不保证有逻辑解。
暂未实现
高端一点的方法是,另写一个逻辑求解子程序,给各种逻辑解法打分,如各种排除法1.x,各种wing2.x,更高的可能有6.x等等。总分数越高,代表越难。
题主所说的保证有逻辑解,并提高难度,肯定不是靠人脑。
而是由机器批量出题,再评定难度。
依陈岑老师的说法,前两年的“最难数独”难度为10.7,现在出现了11+的。可能每出几万道题才能碰上一个8+的。所以只能靠机器了。
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <time.h> #define SIZE 9 int sudoku[SIZE][SIZE];typedef struct Node { struct Node *left , *right , *up , *down ; struct ColumnNode *column ; int row, col; } Node; typedef struct ColumnNode { struct Node *head ; int size; int col; } ColumnNode; ColumnNode *columns[4 *SIZE*SIZE]; Node *root; int answers[81 ];int initConstraint[4 *SIZE*SIZE]={0 };void initLocationDict (int initCount) { srand(time(NULL )); for (int num = 0 ; num < initCount; num++) { while (true ) { printf ("随机生成数字\n" ); int i = rand() % SIZE; int j = rand() % SIZE; int k = rand() % SIZE + 1 ; printf ("num=%d,i=%d,j=%d,k=%d\n" , num, i, j, k); int a = i * 9 + j; printf ("%d\n" , initConstraint[a] ); if (initConstraint[a] == 1 ) continue ; int b = i * 9 + k + 80 ; printf ("%d\n" , initConstraint[b] ); if (initConstraint[b] == 1 ) continue ; int c = j * 9 + k + 161 ; printf ("%d\n" , initConstraint[c] ); if (initConstraint[c] == 1 ) continue ; int d = (i / 3 * 3 + j / 3 ) * 9 + k + 242 ; printf ("%d\n" , initConstraint[d] ); if (initConstraint[d] == 1 ) continue ; initConstraint[a] = 1 ; initConstraint[b] = 1 ; initConstraint[c] = 1 ; initConstraint[d] = 1 ; sudoku[i][j] = k; printf ("随机生成第%d个数字%d,i=%d,j=%d \n" , num, k, i, j); break ; } } } void initialize () { root = (Node *)malloc (sizeof (Node)); root->left = root; root->right = root; root->up = root; root->down = root; Node *prev = root; for (int i = 0 ; i < 4 * SIZE * SIZE; i++) { Node *node = (Node *)malloc (sizeof (Node)); prev->right = node; node->left = prev; node->right = root; node->up = node; node->down = node; node->col = i; columns[i] = (ColumnNode *)malloc (sizeof (ColumnNode)); columns[i]->head = node; columns[i]->size = 0 ; columns[i]->col = i; node->column = columns[i]; prev = node; } } void appendRow (int *cs, int row_id) { Node *prev; Node *first; for (int i = 0 ; i < 4 ; i++) { int col_id = cs[i]; Node *node = (Node *)malloc (sizeof (Node)); Node *head = columns[col_id]->head; Node *tail = head->up; node->row = row_id; node->col = col_id; node->column = columns[col_id]; tail->down = node; node->up = tail; node->down = head; head->up = node; ++columns[col_id]->size; if (i == 0 ) { first = node; node->left = node; node->right = node; prev = node; } else { node->right = prev->right; node->right->left = node; prev->right = node; node->left = prev; } } } void build () { for (int i = 0 ; i < SIZE; i++) { for (int j = 0 ; j < SIZE; j++) { if (sudoku[i][j] != 0 ) { int k = sudoku[i][j]; int cs[4 ]; cs[0 ] = i * 9 + j; cs[1 ] = i * 9 + k + 80 ; cs[2 ] = j * 9 + k + 161 ; cs[3 ] = (i / 3 * 3 + j / 3 ) * 9 + k + 242 ; int row_id = (i * 9 + j) * 9 + k - 1 ; appendRow(cs, row_id); } else { for (int k = 1 ; k < 10 ; k++) { int cs[4 ]; cs[0 ] = i * 9 + j; cs[1 ] = i * 9 + k + 80 ; cs[2 ] = j * 9 + k + 161 ; cs[3 ] = (i / 3 * 3 + j / 3 ) * 9 + k + 242 ; int row_id = (i * 9 + j) * 9 + k - 1 ; appendRow(cs, row_id); } } } } } void removeCol (ColumnNode *columnNode) { Node *head = columnNode->head; head->right->left = head->left; head->left->right = head->right; for (Node *row = head->down; row != head; row = row->down) { for (Node *j = row->right; j != row; j = j->right) { j->down->up = j->up; j->up->down = j->down; columns[j->col]->size--; } } } void restoreCol (ColumnNode *columnNode) { Node *head = columnNode->head; for (Node *row = head->up; row != head; row = row->up) { for (Node *j = row->left; j != row; j = j->left) { j->down->up = j; j->up->down = j; columns[j->col]->size++; } } head->right->left = head; head->left->right = head; } bool dance (int dep) { if (root->right == root) return true ; for (Node *node = root->right; node != root; node = node->right) { if (node->down == node) return false ; } int col = root->right->col; for (Node *node = root->right; node != root; node = node->right) { if (node->column->size < columns[col]->size) col = node->column->col; } ColumnNode *columnNode = columns[col]; removeCol(columnNode); for (Node *row = columnNode->head->down; row != columnNode->head; row = row->down) { answers[dep] = row->row; for (Node *j = row->right; j != row; j = j->right) removeCol(columns[j->col]); if (dance(dep + 1 )) return true ; for (Node *j = row->left; j != row; j = j->left) restoreCol(columns[j->col]); } restoreCol(columnNode); return false ; } void formattedAnswer () { for (int i = 0 ; i < 81 ; i++) { int row_id = answers[i]; int loc = row_id / 9 ; int i = loc / 9 ; int j = loc % 9 ; int k = row_id % 9 + 1 ; sudoku[i][j] = k; } } printsudoku(){ for (int i = 0 ; i < SIZE; i++) { for (int j = 0 ; j < SIZE; j++) { printf ("%d," , sudoku[i][j]); } printf ("\n" ); } } bool checkUniqueSolution () { } int main (int argc, char const *argv[]) { initLocationDict(11 ); printsudoku(); printf ("\n" ); initialize(); build(); while (!dance(0 )) { } formattedAnswer(); printsudoku(); printf ("\n" ); return 0 ; }