1. 程式人生 > 實用技巧 >學習筆記:舞蹈鏈 Dancing Links

學習筆記:舞蹈鏈 Dancing Links

這是一種奇妙的演算法用來解決兩個問題:

  1. 精確覆蓋問題:給定一個矩陣,每行是一個二進位制數,選出儘量少的行,使得每一列恰好有一個 \(1\)
  2. 重複覆蓋問題:給定一個矩陣,每行是一個二進位制數,選出儘量少的行,使得每列至少有一個 \(1\)

模板一般需要有兩個:① 資料結構(十字連結串列)② dfs 框架

其中 ① 對於兩個問題都是一樣的,而 ② 不同問題不同框架。

精確覆蓋問題

1. 如何儲存這個矩陣:十字連結串列

由於 \(1\) 的個數少,我們只存 \(1\),不存 \(0\)

十字連結串列。

  1. 對於所有 \(1\) 的位置建立節點。

  2. 看一下每個節點上下左右應該連向誰,這裡十字連結串列是一個迴圈連結串列。舉個例子,如果 \((x, y)\)

    上面沒有節點,那麼迴圈到最底下,然後再往上走,直到走到第一個 \(1\)

實際到程式碼實現,我們可以按行建立:

  1. 預備資訊
const int N = 具體問題中 1 最多的點數。
int n, m, U[N], D[N], L[N], R[N], idx, s[N], hh, tt, X[N], Y[N];
/* 
n 為行數,m 為列數,U/D/L/R[i] 分別表示 i 節點上下左右的節點。
idx 為當前用了的點數, s[i] 表示第 i 列為 1 的有多少行。
hh, tt 用於加入點時兩個端點。
X[i], Y[i] 表示 i 號點的 x, y 座標
*/
  1. 首先第一行是哨兵(全 \(1\)
    ),並且 \((0, 0)\) 加入一個節點以便後續快速找到現在還有多少個 \(1\)
void inline init() {
	for (int i = 0; i <= m; i++)
		L[i] = i - 1, R[i] = i + 1, U[i] = D[i] = i;
	L[0] = m, R[m] = 0, idx = m;
}
  1. 每一次加入一行。然後考慮左右兩個點 \(hh, tt\),每次動態把一個 \(1\) 插入到 \(hh, tt\) 之間。
void inline add(int x, int y) {
	X[++idx] = x, Y[idx] = y, s[y]++; 
	L[idx] = hh, R[idx] = tt, L[tt] = R[hh] = idx;
	U[idx] = U[y], D[idx] = y, D[U[y]] = idx, U[y] = idx;
	// 注意 U[y] = idx 必須在最後,因為其他東西需要用到之前的 U[y],即 idx 上面那個店。
	hh = idx;
} 
// 按行加入
for (int i = 1; i <= n; i++) {
	hh = idx + 1, tt = idx + 1; // 可以理解為第一行第一個點左右都是自己。
	for (int j = 1, x; j <= m; j++) {
		scanf("%d", &x);
		if (x) add(i, j); // 如果 i, j 為 1,加入 (i, j)
	}
}
  1. 刪除第 \(p\) 列及其關聯的所有行。刪除列的時候,只需要刪除第一行對應的列,原因在於我們 dfs 時找是通過第一行的哨兵找;刪除行的時候,只需要把行對應列的位置刪掉,行相互作用是不需要刪的,原因是不影響,且我們需要用行的資訊恢復現場.jpg
void del(int p) {
	L[R[p]] = L[p], R[L[p]] = R[p];
	for (int i = D[p]; i != p; i = D[i]) {
		for (int j = R[i]; j != i; j = R[j]) {
			s[Y[j]]--, U[D[j]] = U[j], D[U[j]] = D[j];
		}
	}
}
  1. 按插入順序撤銷第 \(p\) 列,注意這裡按照時間逆序操作即可,相互不影響的時間順序可以改變。
void resume(int p) {
	L[R[p]] = p, R[L[p]] = p;
	for (int i = U[p]; i != p; i = U[i]) {
		for (int j = L[i]; j != i; j = L[j]) {
			s[Y[j]]++, U[D[j]] = j, D[U[j]] = j;
		}
	}
}

2. dfs 框架:針對精確覆蓋問題。

每次任意選擇未被選擇的一行,判斷能不能選。

剪枝:

  1. \(1\) 的個數最少的列。列舉這一列選哪個行。如果我們選擇了這一列,接下來就不需要考慮這一列的影響了,所以我們可以把這一列刪掉(十字連結串列的操作)。
  2. 選擇一行,實際上是把這行所有 \(1\) 的列都選了,即刪掉這些列,然後這些列關係到的行也都沒了,把這些行也幹掉,禁止套娃

經過這兩步,我們可以理解為,\(dfs\) 到當前的資料結構上都是沒選的列和行,相當於每次選一行把問題轉化成了更小規模,即選擇一行,把與這行衝突的行刪掉,並且刪掉對應列,通過十字連結串列,我們做到了快速刪除/查詢。

bool inline dfs() {
	if (!R[0]) return true;
	int p = R[0];
	for (int i = R[0]; i; i = R[i])
		if (s[i] < s[p]) p = i;
	if (!s[p]) return false;
	del(p);
	for (int i = D[p]; i != p; i = D[i]) {
		ans[++top] = X[i];
		for (int j = R[i]; j != i; j = R[j]) del(Y[j]);
		if (dfs()) return true;
		for (int j = L[i]; j != i; j = L[j]) resume(Y[j]);
		--top;
	}
	resume(p);
	return false;
}

例題

模板題

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 5505, M = 505;

int n, m, U[N], D[N], L[N], R[N], idx, s[N], hh, tt, X[N], Y[N];

int ans[M], top;

void inline init() {
	for (int i = 0; i <= m; i++)
		L[i] = i - 1, R[i] = i + 1, U[i] = D[i] = i;
	L[0] = m, R[m] = 0, idx = m;
}

void inline add(int x, int y) {
	X[++idx] = x, Y[idx] = y, s[y]++; 
	L[idx] = hh, R[idx] = tt, L[tt] = R[hh] = idx;
	U[idx] = U[y], D[idx] = y, D[U[y]] = idx, U[y] = idx;
	hh = idx;
} 

// 刪除第 p 列

void del(int p) {
	L[R[p]] = L[p], R[L[p]] = R[p];
	for (int i = D[p]; i != p; i = D[i]) {
		for (int j = R[i]; j != i; j = R[j]) {
			s[Y[j]]--, U[D[j]] = U[j], D[U[j]] = D[j];
		}
	}
}

void resume(int p) {
	L[R[p]] = p, R[L[p]] = p;
	for (int i = U[p]; i != p; i = U[i]) {
		for (int j = L[i]; j != i; j = L[j]) {
			s[Y[j]]++, U[D[j]] = j, D[U[j]] = j;
		}
	}
}

bool inline dfs() {
	if (!R[0]) return true;
	int p = R[0];
	for (int i = R[0]; i; i = R[i])
		if (s[i] < s[p]) p = i;
	if (!s[p]) return false;
	del(p);
	for (int i = D[p]; i != p; i = D[i]) {
		ans[++top] = X[i];
		for (int j = R[i]; j != i; j = R[j]) del(Y[j]);
		if (dfs()) return true;
		for (int j = L[i]; j != i; j = L[j]) resume(Y[j]);
		--top;
	}
	resume(p);
	return false;
}

int main() {
	scanf("%d%d", &n, &m);
	init();
	for (int i = 1; i <= n; i++) {
		hh = idx + 1, tt = idx + 1;
		for (int j = 1, x; j <= m; j++) {
			scanf("%d", &x);
			if (x) add(i, j);
		}
	}
	if (!dfs()) puts("No Solution!");
	else for (int i = 1; i <= top; i++) printf("%d ", ans[i]);
	return 0;
}