发布于 ,更新于 

舞蹈链(Dance Link X)算法生成数独和解数独 C语言实现

1. Dancing Links介绍

先看完下面这个文章的精确覆盖问题讲解

2. X 算法

通过上述步骤,可将 X 算法的流程概括如下:

  1. 对于现在的矩阵 M ,选择并标记一行 r,将 r 添加至 S(答案集合) 中;

  2. 如果尝试了所有的 r 却无解,则算法结束,输出无解;

  3. 标记与 r 相关的行 ri 和列 ci

  4. 删除所有标记的行和列,得到新矩阵 M’;

  5. 如果 M’ 为空,且 r 全为1 ,则算法结束,输出被删除的行组成的集合S;
    如果 M’ 为空,且 r 不全为1,则恢复与 r 相关的行 ri 以及列 ci,跳转至步骤 1;
    如果 M’ 不为空,则跳转至步骤 1。

不难看出,X 算法需要大量的「删除行」、「删除列」和「恢复行」、「恢复列」的操作。

一个朴素的想法是,使用一个二维数组存放矩阵,再用四个数组分别存放每一行与之相邻的行编号,每次删除和恢复仅需更新四个数组中的元素。但由于一般问题的矩阵中 0 的数量远多于 1 的数量,这样做的空间复杂度难以接受。

Donald E. Knuth 想到了用双向十字链表来维护这些操作。

而在双向十字链表上不断跳跃的过程被形象地比喻成「跳跃」,因此被用来优化 X 算法的双向十字链表也被称为「Dancing Links」。

3.1 图1

构建的十字循环链表如图1所示,其中columns数组是辅助节点,方便快速定位到相关列,其代码结构如下

辅助数组
1
2
3
4
5
6
7
8
9
// 辅助节点
typedef struct ColumnNode {
struct Node *head;
int size; // 该列下节点数量(不包含head节点)
int col; // 该列的列号
} ColumnNode;

// 辅助约束列,方便定位到哪一列 432个约束条件
ColumnNode *columns[4*SIZE*SIZE];

ColumnNode结构中的head指针指向该列的哨兵节点(十字循环链表的第一行所有节点都是哨兵节点,便于Dance),head的代码结构如下:

DLX节点
1
2
3
4
5
6
// 定义DLX节点结构
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){
// 半删除该列下的head节点
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
// 递归dance
// 如果 0 号结点没有右结点,那么矩阵为空,记录答案并返回;
// 选择列元素个数最少的一列,并删掉这一列;
// 遍历这一列所有有 1 的行,枚举它是否被选择;
// 递归调用 dance(),如果可行,则返回;如果不可行,则恢复被选择的行;
// 如果无解,则返回。
bool dance(int dep){
// 哨兵列为空,说明所有列都有1,所有列的约束都被满足了了
if (root->right == root) return true;

// 如果有某列没有node,即该列没有1,即该列的约束无法满足
for (Node *node = root->right; node != root; node = node->right)
{
if (node->down == node) return false;
}

// 取每列中1的数量最少一列的开始dance
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];

// 定义DLX节点结构
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;

// 辅助约束列,方便定位到哪一列 432个约束条件
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;
}
}

}

// 初始化DLX列节点(哨兵列)
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;
}


}

// 添加[i,j,k]相关的4个约束到十字链表中的同一行
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;// 列元素的数量+1

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;
}
}
}

// 生成DLX十字链表
void build(){
for (int i = 0; i < SIZE; i++)
{
for (int j = 0; j < SIZE; j++)
{
if (sudoku[i][j] != 0) // 11个格子已经随机生成的数字 占用十字链表的11行, 每行4个约束即4个Node
{
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 { // 其他没有生成的70个格子,每个格子有9种可能数字1-9,需要占用十字链表的70*9=630行,每行4个约束即4个Node
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;
}


// 递归dance
// 如果 0 号结点没有右结点,那么矩阵为空,记录答案并返回;
// 选择列元素个数最少的一列,并删掉这一列;
// 遍历这一列所有有 1 的行,枚举它是否被选择;
// 递归调用 dance(),如果可行,则返回;如果不可行,则恢复被选择的行;
// 如果无解,则返回。
bool dance(int dep){
// 哨兵列为空,说明所有列都有1,所有列的约束都被满足了了
if (root->right == root) return true;

// 如果有某列没有node,即该列没有1,即该列的约束无法满足
for (Node *node = root->right; node != root; node = node->right)
{
if (node->down == node) return false;
}

// 取每列中1的数量最少一列的开始dance
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(){
// todo
}


int main(int argc, char const *argv[])
{

initLocationDict(11); // 生成 11个 初始数字,因为11个填入后 能生成数独的概率最大。12个就小一点
printsudoku(); // 打印 填入的11个数字的数独
printf("\n");

initialize(); // 初始化 哨兵列节点 和 辅助列节点
build(); // 构建DLX十字循环链表
//dance(0); // 开始执行DLX算法
while (!dance(0)) // 如果没有解,重新生成,因为即使使用11的初始数字的能生成数独的概率最大,但也不是100%
{
}

formattedAnswer(); // 将DLX的答案转为二维数组(数独形式)
printsudoku(); // 打印最终生的数独
printf("\n");

// 挖洞
// 校验唯一解
// 解法打分,判断难度


return 0;
}