[學習筆記]舞蹈鏈(DLX)(C++指標版)
概述
舞蹈鏈(Dancing Links X)是一種能較高效地解決精確覆蓋問題的暴力演算法
模板題:洛谷P4929
精確覆蓋問題
精確覆蓋問題是這樣一類問題:
給定一個01矩陣,要求選出其中一些行,使得這些行組成的新矩陣每一列恰好有一個1
例如
對於矩陣
\[\left( \begin{matrix} 0 & 0 & 1 & 0 & 1 & 1 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 & 1 & 0 & 1 \end{matrix} \right) \]一組解為1,4,5行
層層深入
基本暴力
列舉所有行組合,然後檢驗
顯然會T到飛起
稍作思考的暴力(X演算法)
假設我們選了一行,那麼與這行有重疊部分的行就不能選了,我們將其刪除
同時,我們把已經被覆蓋的列也刪除
原問題就變成了規模更小的精確覆蓋問題
如果最後沒有辦法覆蓋某一列,就更改之前的操作,繼續暴力
直到最後所有列被覆蓋(找到一組解)或是始終有某些列不能被覆蓋(無解)
以上面的矩陣為例,我們先選擇第1行,刪去這一行和與它有重合部分的行,把已經覆蓋的列也刪去,得到
\[\left( \begin{matrix} 1&0&1&1 \\ 1&0&1&0 \\ 0&1&0&1 \end{matrix} \right) \]接下來問題變成了一個規模更小的精確覆蓋問題
假設我們再選擇第1行,得到
\[\left( \begin{matrix} 1&1 \end{matrix} \right ) \]再選第1行,得到空矩陣
說明找到一組解
回顧過程,我們依次刪掉了原矩陣的1,4,5行
Donald E. Knuth大師的暴力
下面的圖片均來自: https://www.cnblogs.com/grenet/p/3145800.html
上面的稍作思考的暴力複雜度依然很高
為什麼複雜度會很高呢?
我們發現有大量時間花費在對矩陣的修改上
那麼有沒有辦法降低修改矩陣的時間開銷呢?
當然是有的,Donald E. Knuth大師給出瞭解決方案——交叉十字迴圈雙向鏈
首先由於我們只關心覆蓋,所以只考慮1的位置,0就可以丟掉啦
把1的位置像下面這張圖一樣存起來:
對應矩陣
\[\left( \begin{matrix} 0 & 0 & 1 & 0 & 1 & 1 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 1 & 1 & 0 & 1 \end{matrix} \right) \]其中第一行的head是整個連結串列的入口,C1,C2,...是各列的入口
接下來我們來看看具體操作
連結串列構建
首先是連結串列元素的結構體
struct Node {
Node *left, *right, *up, *down, *head; //指向四個方向和列的表頭
int row, cnt; //記錄列表及列元素個數
} *head, *cols[MAXN];
其中cnt只有C1,C2,...的有用,和一個優化有關 ,不加也慢不了多少(少過兩個點而已)
構建連結串列的過程
head = new Node;
for (int i = 0; i < m; ++i) {
cols[i] = new Node;
cols[i]->row = -1;
cols[i]->head = cols[i];
}
for (int i = 1; i < m; ++i) cols[i]->left = cols[i - 1];
for (int i = m - 2; i >= 0; --i) cols[i]->right = cols[i + 1];
cols[0]->left = head, head->right = cols[0];
cols[m - 1]->right = head, head->left = cols[m - 1];
Node *p[MAXN]; //記錄列尾
for (int i = 0; i < m; ++i) p[i] = cols[i];
for (int i = 0; i < n; ++i) {
Node *nplast = NULL;
for (int j = 0; j < m; ++j)
if (mtx[i][j]) {
Node *np = new Node;
np->row = i;
//插入列中
np->up = p[j], p[j]->down = np;
np->down = p[j]->head, p[j]->head->up = np;
np->head = p[j]->head;
//插入行中
if (nplast) {
np->right = nplast->right;
nplast->right->left = np;
np->left = nplast, nplast->right = np;
} else {
np->left = np->right = np;
nplast = np;
}
p[j] = np;
++np->head->cnt;
}
}
選定行
選定某一行時,對照第二種暴力,我們要刪去的有:這一行、這一行能覆蓋的列、與這一行有重疊部分的其它行
比如選定第二行來覆蓋第一列,那麼我們要刪掉下圖中的紫色節點
然後刪除已經覆蓋的列和能覆蓋它們的其它行,即下圖中的橙色節點
最後得到
程式碼如下
// p為要選擇的行的某個元素
for (Node *pp = p->right; pp != p; pp = pp->right) { //找到相關的列
for (Node *ppc = pp->down; ppc != pp; ppc = ppc->down) { // 能覆蓋相關列的行
if (ppc != pp->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down, ppr->down->up = ppr->up;
--ppr->head->cnt;
}
}
pp->head->left->right = pp->head->right;
pp->head->right->left = pp->head->left;
}
for (Node *ppc = p->down; ppc != p; ppc = ppc->down) {
if (ppc != p->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down, ppr->down->up = ppr->up;
--ppr->head->cnt;
}
}
p->head->left->right = p->head->right;
p->head->right->left = p->head->left;
撤銷
就是上面的選行的逆操作
for (Node *pp = p->right; pp != p; pp = pp->right) { //相關的列
for (Node *ppc = pp->down; ppc != pp; ppc = ppc->down) { //包含相關列的行
if (ppc != pp->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down->up = ppr;
++ppr->head->cnt;
}
}
pp->head->left->right = pp->head->right->left = pp->head;
}
for (Node *ppc = p->down; ppc != p; ppc = ppc->down) {
if (ppc != p->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down->up = ppr;
++ppr->head->cnt;
}
}
p->head->left->right = p->head->right->left = p->head;
關於前面提到的優化
每次嘗試覆蓋某一列的時候,我們找剩餘元素最少的那列覆蓋
這樣可以優化一下時間
完整的程式碼:
// luogu P4929
#include <cstdio>
#include <cstring>
#include <iostream>
const int MAXN = 1010;
struct Node {
Node *left, *right, *up, *down, *head; //指向四個方向和列的表頭
int row, cnt; //記錄列表及列元素個數
} *head, *cols[MAXN];
int mtx[MAXN][MAXN], n, m;
int ans[MAXN], top;
void init();
bool solve();
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
scanf("%d", &mtx[i][j]);
init();
if (solve()) {
//puts("One solution found:");
for (int i = 0; i < top; ++i) printf("%d ", ans[i] + 1);
puts("");
} else puts("No Solution!");
return 0;
}
void init() {
top = 0;
head = new Node;
for (int i = 0; i < m; ++i) {
cols[i] = new Node;
cols[i]->row = -1;
cols[i]->head = cols[i];
}
for (int i = 1; i < m; ++i) cols[i]->left = cols[i - 1];
for (int i = m - 2; i >= 0; --i) cols[i]->right = cols[i + 1];
cols[0]->left = head, head->right = cols[0];
cols[m - 1]->right = head, head->left = cols[m - 1];
Node *p[MAXN]; //記錄列尾
for (int i = 0; i < m; ++i) p[i] = cols[i];
for (int i = 0; i < n; ++i) {
Node *nplast = NULL;
for (int j = 0; j < m; ++j)
if (mtx[i][j]) {
Node *np = new Node;
np->row = i;
//插入列中
np->up = p[j], p[j]->down = np;
np->down = p[j]->head, p[j]->head->up = np;
np->head = p[j]->head;
//插入行中
if (nplast) {
np->right = nplast->right;
nplast->right->left = np;
np->left = nplast, nplast->right = np;
} else {
np->left = np->right = np;
nplast = np;
}
p[j] = np;
++np->head->cnt;
}
}
}
bool solve() {
if (head->right == head) return 1; //還有列未被覆蓋
// 找到元素個數最少的列,能有效降低複雜度
Node *c = head->right;
for (Node *p = c->right; p != head; p = p->right)
if (p->cnt < c->cnt) c = p;
for (Node *p = c->down; p != c; p = p->down) {//列舉選擇的行
for (Node *pp = p->right; pp != p; pp = pp->right) { //相關的列
for (Node *ppc = pp->down; ppc != pp; ppc = ppc->down) { //包含相關列的行
if (ppc != pp->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down, ppr->down->up = ppr->up;
--ppr->head->cnt;
}
}
pp->head->left->right = pp->head->right;
pp->head->right->left = pp->head->left;
}
for (Node *ppc = p->down; ppc != p; ppc = ppc->down) {
if (ppc != p->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down, ppr->down->up = ppr->up;
--ppr->head->cnt;
}
}
p->head->left->right = p->head->right;
p->head->right->left = p->head->left;
// 記錄答案,繼續搜尋
ans[top++] = p->row;
if (solve()) return 1;
--top;
// 撤銷選擇行的操作
for (Node *pp = p->right; pp != p; pp = pp->right) { //相關的列
for (Node *ppc = pp->down; ppc != pp; ppc = ppc->down) { //包含相關列的行
if (ppc != pp->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down->up = ppr;
++ppr->head->cnt;
}
}
pp->head->left->right = pp->head->right->left = pp->head;
}
for (Node *ppc = p->down; ppc != p; ppc = ppc->down) {
if (ppc != p->head)
for (Node *ppr = ppc->right; ppr != ppc; ppr = ppr->right) {
ppr->up->down = ppr->down->up = ppr;
++ppr->head->cnt;
}
}
p->head->left->right = p->head->right->left = p->head;
}
return 0;
}
//Rhein_E