1. 程式人生 > 實用技巧 >[學習筆記]舞蹈鏈(DLX)(C++指標版)

[學習筆記]舞蹈鏈(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