给定一个全集 U 和若干集合 S1,S2,S3,…,Sn ,现需从中选取某些集合,不重不漏地覆盖全集 U
或者可以用 0-1 矩阵来表示:
U
1
2
3
4
5
6
7
S1
0
1
0
0
0
1
0
S2
0
0
0
1
1
1
0
S3
0
1
1
0
0
0
1
S4
1
0
0
1
0
0
1
S5
1
0
0
1
0
0
0
S6
0
0
1
0
1
0
1
答案是 S1、S5 和 S6 ,你看看是不是
这种问题的通用解法是 X 算法,我稍微简化了一下过程:
首先按大小顺序依次选取每一行,目前来说也就是第一行 S1
(有个剪枝优化是依次选元素最少列上的非零行,搜索会更快)
U
1
2
3
4
5
6
7
S1
0
1
0
0
0
1
0
S2
0
0
0
1
1
1
0
S3
0
1
1
0
0
0
1
S4
1
0
0
1
0
0
1
S5
1
0
0
1
0
0
0
S6
0
0
1
0
1
0
1
然后找到非空元素的所在列
U
1
2
3
4
5
6
7
S1
0
1
0
0
0
1
0
S2
0
0
0
1
1
1
0
S3
0
1
1
0
0
0
1
S4
1
0
0
1
0
0
1
S5
1
0
0
1
0
0
0
S6
0
0
1
0
1
0
1
然后再顺藤摸瓜地选中目前所有非空元素所在的行
U
1
2
3
4
5
6
7
S1
0
1
0
0
0
1
0
S2
0
0
0
1
1
1
0
S3
0
1
1
0
0
0
1
S4
1
0
0
1
0
0
1
S5
1
0
0
1
0
0
0
S6
0
0
1
0
1
0
1
现在把选中的部分全部删掉,生成一个新矩阵
U
1
3
4
5
7
S4
1
0
1
0
1
S5
1
0
1
0
0
S6
0
1
0
1
1
再选取第一行 S4
U
1
3
4
5
7
S4
1
0
1
0
1
S5
1
0
1
0
0
S6
0
1
0
1
1
重复上面的操作
U
1
3
4
5
7
S4
1
0
1
0
1
S5
1
0
1
0
0
S6
0
1
0
1
1
然后发现删完所有行后 3 和 5 并没有覆盖到,所以这不是一个解
U
3
5
回溯,这次选取第二行,也就是 S5
U
1
3
4
5
7
S4
1
0
1
0
1
S5
1
0
1
0
0
S6
0
1
0
1
1
再重复算法
U
1
3
4
5
7
S4
1
0
1
0
1
S5
1
0
1
0
0
S6
0
1
0
1
1
现在来看就已经很简单了
U
3
5
7
S6
1
1
1
最后再选中 S6 即可
U
3
5
7
S6
1
1
1
全部删完了,得到了一个解
U
还记得前面删除的是哪些行嘛?分别是 S1、S5 和 S6 ,这就是 X 算法
舞蹈链(Dancing Links)简介
现在要写一个程序实现 X 算法 ,但是有一个问题,就是没有合适的数据结构
如果使用二维数组的话,删除行和列,缓存和回溯状态都十分难以实现,而且复杂度堪忧
算法大师 Donald Knuth 为此提出了 Dancing Links(舞蹈链),它是一种链式数据结构,利用链表的性质解决了上述难题。由于删除、恢复等操作是指针之间的跳跃,仿佛是精妙的舞蹈一般,由此得名
而使用 Dancing Links 数据结构的 X 算法 就被成为 Dancing Links X 算法
舞蹈链结构,每行每列都是环形链接,每个结点都有四方向的指针
但是这个图我感觉有点问题,就是行首结点貌似是不在环里的,而且只有向右的指针
分步讲解
整体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
structDLX { int n, m, id; // 行数,列数,最大结点的编号(包含总表头、列首结点和元素结点,行首结点是另外单独存的) int U[N], D[N], L[N], R[N]; // 第i个结点的上下左右结点编号 int Row[N], Col[N]; // 第i个结点的行号,列号 int firstNodePerRow[N]; // 行首结点,指向第i行的第一个元素结点 int nodeNumPerCol[N]; // 第i列的元素结点数目,后面用于剪枝优化 int ans[N]; // 保存解,ans[0]表示总行数,后面依次为结果
voidinit(int n, int m); // 初始化,建立n行m列的DLX矩阵 voidadd(int x, int y); // 在x行y列插入元素结点 voidremove(int c); // 删除列c及其牵扯到的行(其实是隐藏,访问不到就行) voidresume(int c); // 恢复列c及其牵扯到的行 booldance(int deepth); // 递归搜索 voidprint(); // 打印解 }
#include<bits/stdc++.h> usingnamespace std; constint N = 1e5 + 5;
structDLX { int n, m, id; // 行数,列数,最大结点的编号(包含总表头、列首结点和元素结点,行首结点是另外单独存的) int U[N], D[N], L[N], R[N]; // 第i个结点的上下左右结点编号 int Row[N], Col[N]; // 第i个结点的行号,列号 int firstNodePerRow[N]; // 行首结点,指向第i行的第一个元素结点 int nodeNumPerCol[N]; // 第i列的元素结点数目 int ans[N]; // 保存解,ans[0]表示总行数,后面依次为结果
// 初始化,建立n行m列的DLX矩阵 voidinit(int n, int m) { this->n = n; // 行数 this->m = m; // 列数 id = m; // 0是总表头,1~m是列首结点,m+1开始是真实的元素结点
// 写入初始状态数据 for (int i = 0; i <= m; i++) { U[i] = D[i] = i; // 列首结点的上下指针指向自己 L[i] = i - 1; // 列首结点的左指针指向前一个列首结点 R[i] = i + 1; // 列首结点的右指针指向后一个列首结点 }
int n = read(), m = read(); dlx.init(n, m); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) { if (read()) dlx.add(i, j); } dlx.dance(1); dlx.print(); }