舞蹈链(DLX) 笔记
一、精确覆盖问题 & 重复覆盖问题
以下部分例子来自 OI-Wiki。
形式化的语言:有若干个集合
;
感性理解:选择若干个集合,使得选择的不同集合内没有相同的元素,且选择的集合的所有元素恰好就是给定集合
例如,若给出
则
二、X 算法
将每个
问题转换为选择若干行,使得每一列恰好有一个
X 算法的流程如下:先选择一行,通过标记这一行
如上例:
- 选择第一行,删去红色的行(和第一行有相同的
位置)和蓝色的列(无需继续考虑)。
- 选择新矩阵第一行(即原矩阵第二行)。
- 此时发现第二步选择的行并非全为
,说明存在列未被覆盖,回溯,选择第二行(即原矩阵第四行)。
- 此时删掉这剩下的一行(原矩阵第五行),矩阵全空且删除合法,故求出答案
。
三、双向十字链表
其实,可以理解为链式前向星 Pro Max。
双向十字链表有四个指针
此外,它的指针是循环的,也就是说如果向左跳,到了左边界,再往左跳他会跳回右边界,这使得双向十字链表实质上不需要 head,可以从任意位置开始遍历,遍历回自己后结束。
首先,假设需要一个大小为
若
若
若
至于
int r, c, head[N], sz[N], idx;
int X[N], Y[N], L[N], R[N], U[N], D[N];
void build(int n, int m) {
r = n, c = m;
for (int i = 0; i <= c; i++) {
L[i] = i - 1, R[i] = i + 1;
U[i] = D[i] = i;
}
idx = c;
L[0] = c, R[c] = 0;
memset(head, 0, sizeof head);
memset(sz, 0, sizeof sz);
}
void insert(int x, int y) {
X[++idx] = x, Y[idx] = y, sz[y]++;
U[idx] = y, D[idx] = D[y];
U[D[y]] = idx, D[y] = idx;
if (!head[x])
head[x] = L[idx] = R[idx] = idx;
else {
L[idx] = head[x], R[idx] = R[head[x]];
L[R[head[x]]] = idx, R[head[x]] = idx;
}
}四、舞蹈链
对于 X 算法的矩阵,可以只对
1. 精确覆盖问题
写一个 remove 函数,表示删除某一列上有
为什么选择
删除完之后复原即可,如果最后删空了返回可行。
为什么删空了即可?按照代码,如果某一列出现了
如果无法理解代码里的细节,记住双向十字链表是循环的,不会掉出界的,而
void remove(int y) {
L[R[y]] = L[y], R[L[y]] = R[y];
for (int i = D[y]; i != y; i = D[i])
for (int j = R[i]; j != i; j = R[j]) {
U[D[j]] = U[j],
D[U[j]] = D[j];
sz[Y[j]]--;
}
}
void recover(int y) {
for (int i = U[y]; i != y; i = U[i])
for (int j = L[i]; j != i; j = L[j]) {
U[D[j]] = D[U[j]] = j;
sz[Y[j]]++;
}
L[R[y]] = R[L[y]] = y;
}
bool dance(int step) {
if (!R[0]) {
cnt = step;
return true;
}
int y = R[0];
for (int i = R[0]; i; i = R[i])
if (sz[i] < sz[y])
y = i;
remove(y);
for (int i = D[y]; i != y; i = D[i]) {
ans[step + 1] = X[i];
for (int j = R[i]; j != i; j = R[j])
remove(Y[j]);
if (dance(step + 1))
return true;
for (int j = L[i]; j != i; j = L[j])
recover(Y[j]);
}
recover(y);
return false;
}2. 重复覆盖问题
不能删除掉和某一行有冲突的行了,虽然还是可以删掉某一行所有的
重复覆盖问题通常询问最小覆盖集合数,套一个迭代加深即可。
void remove(int x) {
for (int i = D[x]; i != x; i = D[i])
L[R[i]] = L[i], R[L[i]] = R[i];
}
void recover(int x) {
for (int i = U[x]; i != x; i = U[i])
L[R[i]] = R[L[i]] = i;
}
int h() {
int ret = 0;
memset(vis, 0, sizeof vis);
for (int i = R[0]; i; i = R[i]) {
if (vis[i])
continue;
vis[i] = true;
ret++;
for (int j = D[i]; j != i; j = D[j])
for (int k = R[j]; k != j; k = R[k])
vis[Y[k]] = true;
}
return ret;
}
bool dance(int step) {
if (step + h() > mxd)
return false;
if (!R[0])
return true;
int y = R[0];
for (int i = R[0]; i; i = R[i])
if (sz[i] < sz[y])
y = i;
for (int i = D[y]; i != y; i = D[i]) {
ans[step + 1] = X[i];
remove(i);
for (int j = R[i]; j != i; j = R[j])
remove(j);
if (dance(step + 1))
return true;
for (int j = L[i]; j != i; j = L[j])
recover(j);
recover(i);
}
return false;
}
void solve() {
mxd = 0;
while (!dance(0))
mxd++;
cout << mxd << "\n";
}五、刷题套路
这玩意最常用来解决数独类问题,理解了一个就可以理解全部了。
假设当前要解决九行九列数独问题,有行,列,宫的限制,把决策放到行,影响放到列。
如决策在数独第
每一个格子是否放数;每一行,每一列,每一宫是否放
几道题: