1. 程式人生 > >Union-Find 並查集資料結構

Union-Find 並查集資料結構

這節 我們來看看,Union-Find 並查集這種資料結構來。

並查集用於解決快速判斷兩個元素是否來自同一個集合的很高效(FIND操作)以及合併兩個集合的元素(UNION)的資料結構。
並查集廣泛用於 圖的連通性檢查、Kruskal 最小生成樹等問題中,在實際應用以及ACM競賽中用的也是特別多。
根據不同的實現方法,並查集可以取得不同的時間複雜度,其中帶有路徑壓縮以及rank的實現 每次FIND和UNION可以平均取得幾乎常數的時間複雜度。而帶rank的實現每次FIND和UNION可以平均取得O(logn)的複雜度。
在這裡插入圖片描述

其實並查集的思想就是很簡單的:每一個集合使用一個代表元來表示這個集合。

基於陣列的實現

在這裡插入圖片描述
我們給每個集合用一個數字來表示這個集合的代表元,初始化的時候每個元素自成一個集合。這種方式下FIND()很快,只需要O(1)的複雜度。每個元素都需要使用陣列中的某個元素來表示它所在的集合。
但是這種方式的合併就不是很快了,集合A,B合併的時候,需要把其中的一個集合的所有元素對應的標記都做修改。這是需要O(n)的複雜度。
在這裡插入圖片描述

基於樹的實現

既然基於陣列的實現是把每個集合用一個數組來表示,這樣在合併的時候會很低效。那如果我們使用樹來組織一個集合呢?用樹的樹根元素來標識這個集合,每個找某個元素所在的集合的時候都往上遍歷找到它所在樹的根。這樣子 在合併的時候 就只維護好樹根就可以啦。
在這裡插入圖片描述
合併時的虛擬碼:
在這裡插入圖片描述

用樹組織一個集合的確在"合併"的時候很快了,但是嘞,查詢的時候就有一些“意外”情況不好解決了。
例如:
存在如下的一個合併序列:
在這裡插入圖片描述
這個集合的樹就變得相當的不平衡了,此時從FIND( C )的時候需要花費O(n)的時間。相應的,因為UNION的時候需要先找到兩個元素的樹根,招致UNION也需要花費O(n)。

那麼解決方法是什麼呢? 如果我們能夠讓這顆樹“平衡”一下就好啦,不要讓某個集合出現這麼詭異的樹出來。

基於樹+rank的實現

我們給樹的每個節點定義一個rank,這個rank表示以這個節點為樹根的子樹的高度,只含有一個元素的節點的rank為0。合併兩個集合的時候,我們把低rank的節點當做高rank樹根的孩子;如果兩個rank相等,誰成為誰的孩子都沒有關係,但是成為父親的那個節點需要把自己的rank自增1,因為高度增加了。
FIND過程:
在這裡插入圖片描述


UNION過程:
在這裡插入圖片描述
通過這麼做,就可以控制各個樹的深度都不會超過logn。這是為什麼呢?
這是因為,當兩個rank相同的節點合併起來,其中一個節點的rank會加一,當這個節點不在和與他具有相同rank的節點合併時,它的rank不變,所有rank小於它的節點都可以成為它的孩子。於是,一個樹根節點的rank為k,那麼這個樹至少會包含2k2^{k}個節點。顯然,當n個節點全部在同一個集合的時候,這個集合的最大深度為 lognlog n.
取得這個效果還是蠻不錯的了。
但是,我們來看看FIND函式:
在這裡插入圖片描述
對於下面這顆樹:
在這裡插入圖片描述
如果我們執行一次FIND(t),查最下面那個元素的根,那麼它就會沿著 t>s>o>f>at->s->o->f->a 直到樹根a為止。其實,我們可以知道的,從t往a的這一條路徑的所有節點的樹根都是a了。如果我們把這條路徑的所有節點都直接指向a,那麼下次FIND(s),其實應該直接返回a了。那麼 我們怎麼實現這一點呢?

基於樹+rank+路徑壓縮的實現

在這裡插入圖片描述
方法很巧妙,遞迴返回的時候把這個節點的父節點改成根。
直觀上把握,通過這種方式,同一個集合中的很多元素都會直接指向它的樹根,如果此時不是指向樹根,那麼只要一次FIND,那麼它就會直接指向樹根了。
通過藉助勢函式分析,我們可以得到FIND(x)和UNION(x,y)的時間複雜度是lognlog * n。這是什麼鬼東西呢?
我們定義函式f:N>Nf:N->N.
其中f(2)=1f(2)=1,f(n)=1+f(log2(n))f(n)=1+f(log_{2}(n))當n>2時。其實就是一個數n做幾次log運算就可以把它變成1,而這個f就是log*n的定義。
舉個例子:
f(216)=1+f(16)=1+f(24)=2+f(22)=3+f(2)=4f(2^{16})=1+f(16)=1+f(2^4)=2+f(2^2)=3+f(2)=4
f(2216)=f(265536)=1+f(216)f(2^{2^{16}})=f(2^{65536})=1+f(2^{16})=5
em~~~~~ log265536log * 2^{65536}都才5,這得是有多快哦。

應用

NUMBER ISLAND
尋找小島個數。

class Solution {
public:
	struct _point
	{
		int i, j;
		int rank = 0;
		_point(int _i, int _j, int _rank)
			:i(_i), j(_j), rank(_rank)
		{
		}
		bool operator !=(_point const & rhs) const
		{
			return i != rhs.i || j != rhs.j;
		}
		bool operator ==(_point const & rhs) const
		{
			return i == rhs.i && j == rhs.j;
		}
		bool operator < (_point const & rhs) const
		{
			return i < rhs.i || (i==rhs.i && j < rhs.j);
		}
	};
	_point  find(_point x ,vector< vector<_point> > & parent)
	{
		if (parent[x.i][x.j] != x)
		{
			_point rst = find(parent[x.i][x.j],parent);
			parent[x.i][x.j] = rst;
			return rst;
		}
		else
		{
			return parent[x.i][x.j];
		}
	}
	void unions(_point x, _point y, vector<vector <_point> > & parent, set<_point> & island)
	{
		_point xf = find(x, parent);
		_point yf = find(y, parent);
		if (xf == yf)
		{
			return;
		}
		if (xf.rank < yf.rank)
		{
			parent[xf.i][xf.j] = yf;
			island.erase(xf);
			}
		else
		{
			
			if (yf.rank == xf.rank)
			{
				parent[xf.i][xf.j].rank++;
			}
			parent[yf.i][yf.j] = xf;
			island.erase(yf);
		}
	}
	int numIslands(vector<vector<char>>& grid) {
		vector< vector<_point> > parent;
		set< _point> islands;
		for (int i = 0; i < grid.size(); i++)
		{
			parent.push_back(vector<_point>());
			for (int j = 0; j < grid[i].size(); j++)
			{
				parent[i].push_back(_point( i, j, 0 ));
				if (grid[i][j] == '1')
				{
					islands.insert(_point( i, j, 0 ));
					if ((i - 1) >= 0 && grid[i - 1][j] == '1')
					{
						unions(_point( i, j, 0 ), _point( i - 1, j, 0 ), parent, islands);
					}
					if ((j - 1) >= 0 && grid[i][j - 1] == '1')
					{
						unions(_point( i, j, 0 ), _point( i, j - 1, 0 ), parent, islands);
					}

				}
			}
		}
		return islands.size();
	}
	
};

LONGEST CONSECUTIVE SEQUENCE

https://leetcode.com/articles/longest-consecutive-sequence/
在O(n)的時間內,找出數值上連續遞增的最長子序列。
例如:
Input: [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.

解題思路:
對於輸入資料的任意元素x,嘗試讓它與x-1 合併,然後嘗試讓它與x+1合併(如果x-1,x+1都存在的話)。使用並查集來把數值上連續遞增的元素劃分到一個一個集合中,同時,使用並查集來維護各集合中元素個數。

class Solution {
public:
	struct _node
	{
		int parent;
		int num_cnt;
		int rank;
	};
	int find(int x,map<int ,_node> &cnt)
	{
		if (cnt[x].parent != x)
		{
			int rst = find(cnt[x].parent, cnt);
			cnt[x].parent = rst;
			return rst;
		}
		else
		{
			return cnt[x].parent;
		}
	}
	void unions(int x, int y, map<int, _node> & cnt, int &maxcnt)
	{
		int fx = find(x, cnt);
		int fy = find(y, cnt);
		if (fx == fy)
			//相同就不再合併
		{
			return;
		}
		if (cnt[fx].rank < cnt[fy].rank)
		{
			cnt[fx].parent = fy;
			cnt[fy].num_cnt += cnt[fx].num_cnt;
			if (cnt[fy].num_cnt > maxcnt)
			{
				maxcnt = cnt[fy].num_cnt;
			}
		}
		else
		{
			cnt[fy].parent = fx;
			if (cnt[fy].rank == cnt[fx].rank)
			{
				cnt[fx].rank++;
			}
			cnt[fx].num_cnt += cnt[fy].num_cnt;
			if (cnt[fx].num_cnt > maxcnt)
			{
				maxcnt = cnt[fx].num_cnt;
			}
		}
	}
	int longestConsecutive(vector<int>& nums)
	{
		map<int, _node> cnt;
		int maxcnt = nums.size() ? 1 : 0;
		for (int i = 0; i < nums.size(); i++)
		{
			int num = nums[i];
			if (cnt.find(num) == cnt.end())
			{
				cnt[num].num_cnt = 1;
				cnt[num].parent = num;
				cnt[num].rank = 0;
			}
			if (cnt.find(num - 1) != cnt.end())
				//把它和左邊的元素合併
			{
				unions(num, num - 1, cnt, maxcnt);
			}
			if (cnt.find(num + 1) != cnt.end())
				//把它和大於它的元素合併
			{
				unions(num, num + 1, cnt, maxcnt);
			}
		}
		return maxcnt;
	}
};