1. 前述:
在一些有N個元素的集合應用問題中,通常是在開始時讓每個元素構成一個單元素的集合,然後按照一定順序將屬於同一組的元素所在的集合合併,期間要反覆查找一個元素在哪個集合中,這類題目看似並不複雜,但是數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受,即使空間勉強通過,時間複雜度也極高,只能採取一種特殊的數據結構——->並查集
2. 並查集初始化:
一般用樹形結構來組織並查集,類似一顆森林
每一個節點均有一個father[i]來表示它的父親節點
這裏是初始化每個元素,可以使用for循環調用InitSet
初始化集合,目前只有一個元素,根節點爲自身,即x就是根
//並查集(用數組來實現)
int father[N];
void InitSet(int x)
{
//根據實際情況指定的父節點可以變化,這裏使用本身作爲根
father[x] = x;
}
3. 並查集查找
FindSet(x)—–>,查找x所在的集合
若想知道元素3所在的集合,可以通過father[3]=2,father[2]=1,father[1]=1(提前存在的關係)
可以確定元素3所在的集合爲1,可見每次查找元素x所需次數等於元素所在的樹的深度
重點**壓縮路徑優化
1、可以通過壓縮路徑優化,即得到元素3所在的集合爲1時,可以直接father[3]=1
2、查找3的時候會查找2,還會查找其他的元素,可以將這些元素的father[]都設置爲1
3、這樣下一次查找3的子孫所在集合的時候,查找路徑就縮短爲1了
4、當查找3的祖孫所在集合的時候,查找次數爲1,時間複雜度爲O(1)
father[x] = FindSet(father[x]);是遞歸的
回溯時將所有遞歸的元素的father[]均設置爲根節點,這是並查集的優勢所在
int FindSet(int x)
{
if (x != father[x])
{
//壓縮路徑(比較重要)
father[x] = FindSet(father[x]);
}
return father[x];
}
4. 並查集合並
合併x和y(不一定是根節點,得找出x和y節點所在集合的根節點)所在的兩個集合,只需要把其中一個集合的根節點設置爲另一個集合根節點的孩子節點即可
1、假如現在有3個節點。合併1和2可以時這兩種情況,效果一樣,但是再和3合併效果就不同了–>
2、在查找元素3所在的集合時,前者(沒有使用壓縮路徑優化的)要通過father[3]=2,father[2]=1,father[1]=1來找
3、但是後者(使用壓縮路徑優化)只需要一次查找即可,後者要優於前者
4、所以在合併時有一個啓發式合併的優化(即記錄每個集合的深度(可以是集合元素的個數))
5、合併時,將深度小的掛在深度大的集合下面就可
void Union(int x, int y)
{
//首先要找到元素x和y所在集合的根節點
int fx = FindSet(x);
int fy = FindSet(y);
if (fx == fy) //兩個集合相等,是一個集合
return;
//兩個集合不相等rank[fx]是fx這個集合的深度
if (rank[fx] > rank[fy]) //fy合併到fx
{
father[fy] = fx; //將fy掛在fx下面
if (rank[fy]+1 > rank[fx]) //這裏的秩爲深度
rank[fx] = rank[fy] + 1; //更新fx的深度
}
else //fx合併到fy
{
father[fx] = fy;
if (rank[fx]+1 > rank[fy])
rank[fy] = rank[fx] + 1;
}
}
5. 總結:
上面的這個用來啓發式合併秩可以自己改變,總之能使問題優化即可,這個秩也可設爲集合中元素的個數
注意:僅僅只有根節點的rank[]保存的值纔是正確的,rank[x]不等rank[fx]
可以通過查找到x所在的集合的根節點fx,並查集適用於所有集合的合併和查找,由於使用啓發式合併和路徑壓縮技術,可以將並查集的時間複雜度看爲O(1),空間複雜度看爲O(n)