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,那麼這個樹至少會包含個節點。顯然,當n個節點全部在同一個集合的時候,這個集合的最大深度為 .
取得這個效果還是蠻不錯的了。
但是,我們來看看FIND函式:
對於下面這顆樹:
如果我們執行一次FIND(t),查最下面那個元素的根,那麼它就會沿著 直到樹根a為止。其實,我們可以知道的,從t往a的這一條路徑的所有節點的樹根都是a了。如果我們把這條路徑的所有節點都直接指向a,那麼下次FIND(s),其實應該直接返回a了。那麼 我們怎麼實現這一點呢?
基於樹+rank+路徑壓縮的實現
方法很巧妙,遞迴返回的時候把這個節點的父節點改成根。
直觀上把握,通過這種方式,同一個集合中的很多元素都會直接指向它的樹根,如果此時不是指向樹根,那麼只要一次FIND,那麼它就會直接指向樹根了。
通過藉助勢函式分析,我們可以得到FIND(x)和UNION(x,y)的時間複雜度是。這是什麼鬼東西呢?
我們定義函式.
其中,當n>2時。其實就是一個數n做幾次log運算就可以把它變成1,而這個f就是log*n的定義。
舉個例子:
=5
em~~~~~ 都才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;
}
};