《資料結構與演算法分析》學習筆記-第八章-不相交集

CrazyCatJack發表於2021-02-21


對於每一對元素(a,b), a, b屬於S,aRb或者為true或者為false,則稱在集合S上定義關係R,如果aRb是true。那麼我們說a與b有關係。

8.1 等價關係

等價關係是滿足下列三個性質的關係R:

  1. (自反性)對於所有的a屬於S,aRa
  2. (對稱性)aRb當且僅當bRa
  3. (傳遞性)若aRb且bRc,則aRc

關係≤不是等價關係,因為他不是對稱的,因為從a≤b,不能得出b≤a;電氣連通性是一個等價關係。如果兩個城市位於同一個國家,那麼定義他們是有關係的,容易驗證這是一個等價關係。

8.2 動態等價性問題

  • Find運算返回包含給定元素的集合,即等價類的名字。Union運算即求並運算,可以將a和b的兩個等價類合併成一個新的的等價類。Sk = Si U Sj。該演算法是動態的,因為在演算法執行的過程中,集合可以通過Union運算而發生改變。這個運算還必然是聯機操作,當Find執行時,它必須給出答案演算法才能繼續進行,另一種是離線演算法,它需要觀察全部的Union和Find序列。他對每個Find給出的答案必須和所有執行到該Find的Union一致。而該演算法在看到所有的問題以後再給出它的所有的答案。這種差別類似於參加一次筆試(它一般是離線的,你只能在規定的時間用完之前給出的答卷)和一次口試(它是聯機的,因為你必須回答當前的問題,然後才能繼續下一個問題)。
  • 解決動態等價問題的方案有兩種。一種方案保證指令Find能夠以常數最壞情形執行時間執行;另一種方案則保證指令Union能夠以常數最壞情形時間執行。二者不能同時做到。為使Find運算快,可以在一個陣列中儲存每個元素的等價類的名字,此時Find就是簡單的O(1)查詢。設我們想要執行Union(a,b),並設a在等價類i中而b在等價類j中。然後掃描陣列,將所有的i變成j.這個掃描耗費O(N)時間。於是,連續N-1次Union操作(這是最大值,因為此時每個元素都在一個集合中)就要花費O(N^2) 時間,如果存在Ω(N^2) 次Find運算,那麼效能會很好。因為在整個演算法進行過程中每個Find或Union運算的總的執行時間為O(1),如果Find運算沒有那麼多,那麼這個界是不可接受的。一種想法是將所有在同一個等價類中的元素放到一個連結串列中,者在更新的時候會節省時間,因為我們不必搜尋整個陣列,但由於他在演算法過程中仍然可能執行Θ(N^2)次等價更新。每個元素可能將他的等價類最多改變logN次,因為每次它的等價類改變時它的新的等價類至少是他的原來等價類的兩倍大,任意順序的M次Find和直到N-1次的Union最多話費O(M + NlogN)時間。

8.3 基本資料結構

為了執行兩個集合的Union運算,我們使一個結點的根指標指向另一棵樹的根節點。這種操作花費常數時間。對元素X的一次Find(X)操作通過返回包含X的樹的根而完成。執行這次操作花費時間與表示X的節點的深度成正比。這要假設我們以常數時間找到表示X的節點。用這種方法能夠建立一棵深度為N-1的樹,使得一次Find的最壞情形執行時間是O(N),一般情況,執行時間是對連續混合使用M個指令來計算的。這種情況下,M次連續操作在最壞情形下可能話費O(MN)時間

實現

  1. 型別宣告
#ifndef _DisjSet-H
typedef int DisjSet[NumSets + 1];
typedef int SetType;
typedef int ElementType;

void Initialize(DisjSet S);
void SetUnion(DisjSet S, SetType Root1, SetType Root2);
SetType Find(ElementType X, DisjSet S);

#endif
  1. Initialize
void
Initialize(DisjSet S)
{
    int i;
    for (i = NumSets; i > 0; i--)
    {
        S[i] = 0;
    }
}
  1. SetUnion
void
SetUnion(DisjSet S, SetType Root1, SetType Root2)
{
    S[Root2] = Root1;
}
  1. Find
SetType
Find(ElementType X, DisjSet S)
{
    if (S[X] <= 0)
        return X;
    else
        return Find(S[X], S);
}

8.4 靈巧求並演算法

  • 使用一種方法,總是讓較小的樹成為較大的樹的子樹,即按大小求並。這樣會避免形成較深的樹。如果按大小求並,那麼任何結點的深度均不會超過logN。節點初始處於深度0的位置,當它的深度隨著一次Union的結果而增加,該節點則被置於至少是它以前所在樹兩倍大的一棵樹上。因此,它的深度最多可以增加logN次。Find操作的執行時間是O(logN),而連續M次操作則花費O(MlogN)。需要記住每一棵樹的大小,由於實際上只使用一個陣列,因此可以讓每個根的陣列元素包含它的樹的大小的負值。初始時,樹的陣列表示就都是-1了。當執行一次Union時,要檢查樹的大小,新的大小是老的大小的和。這樣,按大小求並的實現根本不存在困難。並且不需要額外的空間,其平均速度也很快。若使用按大小求並則連續M次運算需要O(M)平均時間。這是因為當隨機的諸Union執行時整個演算法一般只有一些很小的集合(通常含一個元素)與大集合合併。
  • 另一種實現方法為按高度求並。它同樣保證所有的樹的深度最多是O(logN)。我們跟蹤每棵樹的高度而不是大小並執行那些Union使得淺的樹成為深的樹的子樹。這是一種平緩的演算法,因為只有當兩棵相等深度的樹求並時樹的高度才增加。按高度求並是按大小求並的簡單修改。
void
SetUnion(DisjSet S, SetType Root1, SetType Root2)
{
    if (S[Root2] < S[Root1]) /* Root2 is deeper set */
    {
        S[Root1] = Root2;
    }
    else
    {
        if (S[Root1] == S[Root2]) /* Same height */
        {
            S[Root1]--; /* so update */
        }
        S[Root2] = Root1;
    }
}

8.5 路徑壓縮

路徑壓縮在一次Find操作期間執行,而與用來執行Union的方法無關。設操作為Find(X),此時路徑壓縮的效果是,從X到根的路徑上的每一個節點都使它的父節點變成根。

SetType
Find(ElementType X, DisjSet S)
{
    if (S[X] <= 0)
    {
        return X;
    }
    else
    {
        return S[X] = Find(S[X], S);
    }
}

當執行一些Union操作的時候,路徑壓縮是個好的想法,因為存在許多的深層節點並通過路徑壓縮將他們移近根節點。在這種情況下進行路徑壓縮時,連續M次操作最多需要O(MlogN)的時間。路徑壓縮與按大小求並完全相容,與按高度求並不完全相容

8.7 一個應用

我們有一個計算機網路和一個雙向連線表。每一個連線可將檔案從一臺計算機傳送到另一臺計算機。如何將任意一個檔案傳送給任意一個計算機。解決該問題的一個演算法時開始時把每一臺計算機放到它自己的集合中。我們要求兩臺計算機可以傳輸檔案當且僅當他們在同一個集合中。可以看出,傳輸檔案的能力形成一個等價關係。我們一次一個的讀入連線,當讀入某個連結(u, v),則測試是否u和v在一個集合中。如果在一個集合中則什麼都不做,如果在不同的集合中,那我們將他們所在的兩個集合合併。在演算法的最後,所得到的圖聯通當且僅當恰好存在一個集合。如果存在M個連線和N臺計算機,那麼空間的需求則是O(N)。使用按大小求並和路徑壓縮的方法,我們得到最壞情形執行時間為O(M a(M, N)),因為存在2M次Find和至多N-1次Union.這個執行時間在實用中是線性的

參考文獻

  1. Mark Allen Weiss.資料結構與演算法分析[M].America, 2007

本文作者: CrazyCatJack

本文連結: https://www.cnblogs.com/CrazyCatJack/p/14408186.html

版權宣告:本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

關注博主:如果您覺得該文章對您有幫助,可以點選文章右下角推薦一下,您的支援將成為我最大的動力!


相關文章