等價關系與等價類
若對於每一對元素(a,b),a,b∈S,a R b或者為true或者為false,則稱在集合S上定義關系R。如果a R b為true,那么我們說a與b有關系。
等價關系(equivalence relation)是滿足下列三個性質的關系R:
(1) 自反性:對於所有a∈S,a R a
(2) 對稱性:若a R b當且僅當b R a
(3) 傳遞性:若a R b且b R c 則a R c
關系“≤”不是等價關系。雖然它是自反的(即a≤a)、可傳遞的(即由a≤b和b≤c得出a≤c),但它不是對稱的,因為a≤b不能得出b≤a。
如果兩個城市位於同一個國家,那么定義它們是由關系的。容易驗證這是一個等價關系。如果能夠通過公路從a旅行到b,則設a與b有關系。如果所有的道路都是雙向行駛的,那么這種關系也是一個等價關系。
並查集:
在計算機科學中,並查集是一種樹形的數據結構,其保持着用於處理一些不相交集合(Disjoint Sets)的合並及查詢問題。常常在使用中以森林來表示,進行快速規整。
並查集保持一組不相交的動態集合S={S1,S2,...,Sk}。每個集合通過一個代表來識別,代表即集合中的某個成員。在某些應用中,哪一個成員被選作代表無所謂。在一些應用中,如何選擇代表可能存在預先說明的規則,例如選擇集合中的最小元素。
並查集提供以下操作:
1. Find:確定元素屬於哪一個集合。不要求find操作返回任何特定的名字,而只要求當且僅當兩個元素屬於相同的集合時,作用在這兩個元素上的find返回相同的名字。
2. Union:將兩個子集合並成同一個集合
3. MakeSet: 用於建立單元素集合。
並查集的表示方法:
1. 單鏈表表示
要實現並查集數據結構,一種簡單的方法是每一個集合都用一個鏈表來表示。每個鏈表的第一個對象作為它所在集合的代表。鏈表中的每一個對象都包含一個集合成員、一個指向包含下一個集合成員的對象的指針,以及指向代表的指針。每個鏈表都含head指針和tail指針,head指向鏈表的代表,tail指向鏈表中最后的對象。
2. 並查集森林
並查集的另一種更快的實現是用有根樹表示集合樹中每個節點包含一個成員,每棵樹代表一個集合。在一個不相交森林中,每個成員僅指向指向它的父節點。每棵樹的根包含集合的代表,並且是其自己的父節點。正如我們將要看到的那樣,雖然使用這種表示的直接算法並不比使用鏈表表示的算法快,但通過引入兩種啟發式策略(“按秩合並”和“路徑壓縮"),我們能得到一個漸進最優的不相交集合數據結構。
在這種表示方法中,MAKE-SET操作簡單地創建一棵只有一個節點的樹,Find操作通過沿着指向父節點的指針找到樹的根。這一通過根節點的簡單路徑上所訪問過的節點構成了查找路徑。union操作使得一棵樹指向另一棵樹的根。下圖所示為union操作:
開始時每個集合含有一個元素。這些樹不一定必須是二叉樹,但是用二叉樹表示起來要容易,因為我們需要的唯一信息就是父鏈(parent link)。集合的名字由根處的節點給出。由於只需要父節點的名字,因此可以假設這棵樹是被非顯式的存儲在數組中(與二叉堆類似):數組的每個成員s[i]表示元素i的父親。如果i是根,那么s[i]=-1.、
並查集森林的兩種改進策略:
1. 按秩合並(union by rank)。改進union操作的方法。這種方法的思想是在union操作中使較少節點的樹的根指向具有較多節點的樹的根,使得總是較小的樹成為較大的樹的子樹。注意,這里並不顯式地記錄每個節點為根的子樹的大小,而是采用一種易於分析的方法。對於每個節點,維護一個秩,它表示該節點高度的一個上界。在使用按秩合並策略的UNION操作中,我們可以讓具有較小秩(高度)的根指向具有較大秩的根。我們初始化數組的所有項為-1。下圖顯示了一棵樹及其對於按秩求並的實例。
2. 路徑壓縮(path compression)。改進find操作的方法。它在查找過程中將查到節點到根的每個節點的父節點直接指向根。如下所示
實現代碼:
class DisjSets { public: explicit DisjSets(int numElements); int Find(int x); void Union(int element1, int element2); // union is key word in C++, use Union private: vector<int> s; }; DisjSets::DisjSets(int numElements): s(numElements) { for (vector<int>::iterator iter = s.begin(); iter != s.end(); ++iter) { *iter = -1; } } //路徑壓縮 int DisjSets::Find(int x) { if (s[x] < 0) return x; else return s[x] = Find(s[x]); } void DisjSets::Union(int element1, int element2) { int root1 = Find(element1); //find root of element1 int root2 = Find(element2); //find root of element2 if (s[root2] < s[root1]) { s[root1] = root2; } else { if (s[root1] == s[root2]) s[root1]--; s[root2] = root1; } }
參考資料:
1. 《算法導論》(第三版)
2. 《數據結構與算法分析C++描述》(第三版)