這一篇我們看看經典又神奇的並查集,顧名思義就是並起來查,可用於處理一些不相交集合的秒殺。
一:場景
有時候我們會遇到這樣的場景,比如:M={1,4,6,8},N={2,4,5,7},我的需求就是判斷{1,2}是否屬於同一個集合,當然實現方法
有很多,一般情況下,普通青年會做出O(MN)的復雜度,那么有沒有更輕量級的復雜度呢?嘿嘿,並查集就是用來解決這個問題的。
二:操作
從名字可以出來,並查集其實只有兩種操作,並(Union)和查(Find),並查集是一種算法,所以我們要給它選擇一個好的數據結構,
通常我們用樹來作為它的底層實現。
1.節點定義
1 #region 樹節點
2 /// <summary>
3 /// 樹節點
4 /// </summary>
5 public class Node
6 {
7 /// <summary>
8 /// 父節點
9 /// </summary>
10 public char parent;
11
12 /// <summary>
13 /// 節點的秩
14 /// </summary>
15 public int rank;
16 }
17 #endregion
2.Union操作
<1>原始方案
首先我們會對集合的所有元素進行打散,最后每個元素都是一個獨根的樹,然后我們Union其中某兩個元素,讓他們成為一個集合,
最壞情況下我們進行M次的Union時會存在這樣的一個鏈表的場景。

從圖中我們可以看到,Union時出現了最壞的情況,而且這種情況還是比較容易出現的,最終導致在Find的時候就相當寒酸苦逼了,為O(N)。
<2> 按秩合並
我們發現出現這種情況的原因在於我們Union時都是將合並后的大樹作為小樹的孩子節點存在,那么我們在Union時能不能判斷一下,
將小樹作為大樹的孩子節點存在,最終也就降低了新樹的深度,比如圖中的Union(D,{E,F})的時候可以做出如下修改。

可以看出,我們有效的降低了樹的深度,在N個元素的集合中,構建樹的深度不會超過LogN層。M次操作的復雜度為O(MlogN),從代
碼上來說,我們用Rank來統計樹的秩,可以理解為樹的高度,獨根樹時Rank=0,當兩棵樹的Rank相同時,可以隨意挑選合並,在新
根中的Rank++就可以了。
1 #region 合並兩個不相交集合
2 /// <summary>
3 /// 合並兩個不相交集合
4 /// </summary>
5 /// <param name="root1"></param>
6 /// <param name="root2"></param>
7 /// <returns></returns>
8 public void Union(char root1, char root2)
9 {
10 char x1 = Find(root1);
11 char y1 = Find(root2);
12
13 //如果根節點相同則說明是同一個集合
14 if (x1 == y1)
15 return;
16
17 //說明左集合的深度 < 右集合
18 if (dic[x1].rank < dic[y1].rank)
19 {
20 //將左集合指向右集合
21 dic[x1].parent = y1;
22 }
23 else
24 {
25 //如果 秩 相等,則將 y1 並入到 x1 中,並將x1++
26 if (dic[x1].rank == dic[y1].rank)
27 dic[x1].rank++;
28
29 dic[y1].parent = x1;
30 }
31 }
32 #endregion
3.Find操作
我們學算法,都希望能把一個問題優化到地球人都不能優化的地步,針對logN的級別,我們還能優化嗎?當然可以。
<1>路徑壓縮
在Union和Find這兩種操作中,顯然我們在Union上面已經做到了極致,下面我們在Find上面考慮一下,是不是可以在Find上運用
伸展樹的思想,這種伸展思想就是壓縮路徑。

從圖中我們可以看出,當我Find(F)的時候,找到“F”后,我們開始一直回溯,在回溯的過程中給,把該節點的父親指向根節點。最終
我們會形成一個壓縮后的樹,當我們再次Find(F)的時候,只要O(1)的時間就可以獲取,這里有個注意的地方就是Rank,當我們在路
徑壓縮時,最后樹的高度可能會降低,可能你會意識到原先的Rank就需要修改了,所以我要說的就是,當路徑壓縮時,Rank保存的就
是樹高度的上界,而不僅僅是明確的樹高度,可以理解成"伸縮椅"伸時候的長度。
1 #region 查找x所屬的集合
2 /// <summary>
3 /// 查找x所屬的集合
4 /// </summary>
5 /// <param name="x"></param>
6 /// <returns></returns>
7 public char Find(char x)
8 {
9 //如果相等,則說明已經到根節點了,返回根節點元素
10 if (dic[x].parent == x)
11 return x;
12
13 //路徑壓縮(回溯的時候賦值,最終的值就是上面返回的"x",也就是一條路徑上全部被修改了)
14 return dic[x].parent = Find(dic[x].parent);
15 }
16 #endregion
我們注意到,在路徑壓縮后,我們將LogN的復雜度降低到Alpha(N),Alpha(N)可以理解成一個比hash函數還有小的常量,嘿嘿,這
就是算法的魅力。
最后上一下總的運行代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
//定義 6 個節點
char[] c = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
DisjointSet set = new DisjointSet();
set.Init(c);
set.Union('E', 'F');
set.Union('C', 'D');
set.Union('C', 'E');
var b = set.IsSameSet('C', 'E');
Console.WriteLine("C,E是否在同一個集合:{0}", b);
b = set.IsSameSet('A', 'C');
Console.WriteLine("A,C是否在同一個集合:{0}", b);
Console.Read();
}
}
/// <summary>
/// 並查集
/// </summary>
public class DisjointSet
{
#region 樹節點
/// <summary>
/// 樹節點
/// </summary>
public class Node
{
/// <summary>
/// 父節點
/// </summary>
public char parent;
/// <summary>
/// 節點的秩
/// </summary>
public int rank;
}
#endregion
Dictionary<char, Node> dic = new Dictionary<char, Node>();
#region 做單一集合的初始化操作
/// <summary>
/// 做單一集合的初始化操作
/// </summary>
public void Init(char[] c)
{
//默認的不想交集合的父節點指向自己
for (int i = 0; i < c.Length; i++)
{
dic.Add(c[i], new Node()
{
parent = c[i],
rank = 0
});
}
}
#endregion
#region 判斷兩元素是否屬於同一個集合
/// <summary>
/// 判斷兩元素是否屬於同一個集合
/// </summary>
/// <param name="root1"></param>
/// <param name="root2"></param>
/// <returns></returns>
public bool IsSameSet(char root1, char root2)
{
return Find(root1) == Find(root2);
}
#endregion
#region 查找x所屬的集合
/// <summary>
/// 查找x所屬的集合
/// </summary>
/// <param name="x"></param>
/// <returns></returns>
public char Find(char x)
{
//如果相等,則說明已經到根節點了,返回根節點元素
if (dic[x].parent == x)
return x;
//路徑壓縮(回溯的時候賦值,最終的值就是上面返回的"x",也就是一條路徑上全部被修改了)
return dic[x].parent = Find(dic[x].parent);
}
#endregion
#region 合並兩個不相交集合
/// <summary>
/// 合並兩個不相交集合
/// </summary>
/// <param name="root1"></param>
/// <param name="root2"></param>
/// <returns></returns>
public void Union(char root1, char root2)
{
char x1 = Find(root1);
char y1 = Find(root2);
//如果根節點相同則說明是同一個集合
if (x1 == y1)
return;
//說明左集合的深度 < 右集合
if (dic[x1].rank < dic[y1].rank)
{
//將左集合指向右集合
dic[x1].parent = y1;
}
else
{
//如果 秩 相等,則將 y1 並入到 x1 中,並將x1++
if (dic[x1].rank == dic[y1].rank)
dic[x1].rank++;
dic[y1].parent = x1;
}
}
#endregion
}
}

