圖論連通性相關

end_switch發表於2024-09-04

並查集

普通並查集:

路徑壓縮寫法:

struct Union_Find_Set {
	int f[N];
	inline void init() {
		for(int i = 1 ; i <= n ; ++ i)
			f[i] = i;
	}
	inline int find(int x) {
		if(x != f[x]) f[x] = find(f[x]);
		return f[x];
	}
	inline void merge(int a, int b) {
		int x = find(a), y = find(b);
		f[y] = f[x];
	}
} Set;

啟發式合併寫法:

struct Union_Find_Set {
	int f[N], h[N];
	inline void init() {
		for(int i = 1 ; i <= n ; ++ i)
			f[i] = i, h[i] = 1;
	}
	inline int find(int x) {
		if(x != f[x]) return find(f[x]);
		return f[x];
	}
	inline void merge(int a, int b) {
		int x = find(a), y = find(b);
		if(h[x] > h[y]) f[y] = f[x];
		else {
			f[x] = f[y];
			if(h[x] == h[y]) ++ h[y];
		}
	}
} Set;

這是按秩合併的,當然也可以按元素個數啟發式合併。

這倆可以寫一起:

struct Union_Find_Set {
	int f[N], h[N];
	inline void init() {
		for(int i = 1 ; i <= n ; ++ i)
			f[i] = i, h[i] = 1;
	}
	inline int find(int x) {
		if(x != f[x]) f[x] = find(f[x]);
		return f[x];
	}
	inline void merge(int a, int b) {
		int x = find(a), y = find(b);
		if(h[x] > h[y]) f[y] = f[x];
		else {
			f[x] = f[y];
			if(h[x] == h[y]) ++ h[y];
		}
	}
} Set;

要注意的是 find 裡面的 ifreturn 的複雜度是假的。

例題

多而且雜,一般可用並查集維護的性質很突出。

CF217A Ice Skating

vjudge

很傻逼的網格圖問題。

同列 / 同行放到一個連通塊裡面,並查集輕鬆維護。

P2658 汽車拉力比賽

有點傻逼的網格圖問題。

分析:

1.由於題目要求保證所有路標相互可達,於是想到並查集

2.發現對於任意一個 \(D\),模擬一次都是 \(O(nm)\) 的,且 \(D\) 越大所有路標越可能相互可達,考慮二分 \(D\)

3.每次掃網格圖,對於每個點,若和其相鄰點高度差小於當前二分的 \(D\) 則連邊,最後判斷所有路標是否在一個連通塊內

4.座標 \((x, y)\) 可以改寫成 \((x - 1)m + y\)

時間複雜度 \(O(nm\log V)\),其中 \(V\) 為值域。

check 部分程式碼:

inline bool check(int x) {
	init();
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j) {
			//to(i, j) 為座標的轉換函式。
			
			int pos = to(i, j), pos1 = to(i + 1, j), pos2 = to(i, j + 1);
			
			if(i + 1 <= n && abs(a[pos] - a[pos1]) <= x) merge(pos, pos1);
			if(j + 1 <= m && abs(a[pos] - a[pos2]) <= x) merge(pos, pos2);
		}
	
	int Fa = 0;
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			if(vis[to(i, j)]) {
				if(! Fa) Fa = find(to(i, j));
				else if(Fa != find(to(i, j))) return false;
			}
	
	return true;
}

擴充套件域並查集:

應用於有多個集合且有關係時。

另外這東西還能判二分圖。

具體的就是建多個並查集。

帶權並查集:

維護邊的時候帶權。

一般用路徑壓縮能夠減少維護的資訊。

合併時候的權值更新可以用向量去理解。

struct Union_Find_Set {
	int f[N], val[N];
	inline void init() {
		memset(val, 0, sizeof val);
		for(int i = 1 ; i <= n ; ++ i)
			f[i] = i;
	}
	inline int find(int x) {
		if(x != f[x]) val[x] += val[f[x]], f[x] = find(f[x]);
		return f[x];
	}
	inline void merge(int a, int b, int Val) {
		int x = find(a), y = find(b);
		f[y] = f[x], val[y] = -val[a] + Val + val[b];
	}
} Set;

當然操作不僅限於加法。

可撤銷並查集

按加入的時間從後往前撤銷。

用啟發式合併寫法實現(路徑壓縮改變樹的形態),同時維護上述操作可以用棧來實現。

那麼對於一條邊為什麼一定要是有順序的撤銷呢?如果不是按出棧的順序撤銷,那麼必定有比他晚一些連邊的集合的大小沒法維護,所以必須按出棧順序撤銷。

struct Union_Find_Set {
	int f[N], h[N];
	stack<int> s;
	inline void init() {
		memset(val, 0, sizeof val);
		for(int i = 1 ; i <= n ; ++ i)
			f[i] = i, h[i] = 1;
	}
	inline int find(int x) {
		if(x != f[x]) return find(f[x]);
		return f[x];
	}
	inline void merge(int a, int b) {
		int x = find(a), y = find(b);
		if(h[x] > h[y]) f[y] = f[x];
		else {
			f[x] = f[y];
			if(h[x] == h[y]) ++ h[y];
		}
	}
	inline void Delete() {
		if(s.empty()) return;
		int k = s.top(); s.pop();
		h[f[k]] -= h[k], f[k] = k;
	}
	inline void revoke(int x) {while(s.size() > x) Delete();}
} Set;

相關文章