演算法:互斥集合

CopperDong發表於2017-09-06

    表示互斥集合(disjoint set)時,經常會使用另一種具有獨特形態的樹結構--並查集(union-find)資料結構。

互斥集合:

        假設有n名客人蔘加聚會,主持人要求相同生日的人組成一隊。話音剛落,客人們立刻開始組隊。剛開始時,因為不知道哪位客人的生日與自己的生日相同,所以大家只能單獨徘徊。不過,只要找到1名相同生日的客人,兩人就會開始結伴移動。若發現另一個相同生日的隊伍,就會與這支隊伍合併。

        應該使用何種資料結構表示這種情況呢?對參加聚會的全體客人的集合而言,隊伍是將些集合分割為若干部分的子集合。而每個隊由生日相同的客人組成,所以每名客人都不可能屬於一個以上的隊伍。這種沒有共同元素的子集合稱為互斥子集合,儲存這種資訊並對其進行操作的資料結構就是並杳集資料結構。

       為了表示這種情況,首先需要把客人表示為0到n-1之間的元素,然後生成只包含1個元素的n個集合。兩名客人a和b的生日相同時,合併包含二者的集合。為了實現該過程,需要如下3種運算。

      1、初始化:初始化為n個元素被包含於各自集合的形式

      2、並集(union)運算:給出兩個元素a和b時,合併兩個元素所在集合

      3、查詢(find)運算:給出了某個元素a時,返回此元素所在集合

利用陣列表示互斥集合:

     利用一維陣列就能非常簡單地表示互斥集合。生成如下形式的陣列belongsTo。

             belongsTo[i] = 第i個元素所屬的集合序號

     此方式並集運算耗時,雖然查詢O(1)

利用樹結構表示互斥集合:

     同屬於一個集合的元素繫結到一個樹結構。

struct NaiveDisjointSet {

    vector<int>   parent;

    NaiveDisjointSet(int n) : parent(n) {

        for (int i=0; i<n; i++)    parent[i] = i;

    }

    int  find(int u)  const  {

        if (u == parent[u])  return u;

        return find(parent[u]);

    }

    void merge(int u, int v) {

         u = find(u);   v = find(v);

         if (u == v)    return;

         parent[u] = v;

     }

};

互斥集合的優化:

     如果最後生成高度為n-1的樹,則是連結串列,那麼合併和查詢運算都會耗費O(n)的運算時間。

     最簡單的方法是,合併兩個樹時,把高度更低的樹新增到高度更高的樹,以限制樹高的增加

struct OptimizedDisjointSet {

    vector<int>   parent, rank;

    NaiveDisjointSet(int n) : parent(n) , rank(n, 1) {

        for (int i=0; i<n; i++)    parent[i] = i;

    }

    int  find(int u)  const  {

        if (u == parent[u])  return u;

        return parent[u] = find(parent[u]);

    }

    void merge(int u, int v) {

         u = find(u);   v = find(v);

         if (u == v)    return;

        if (rank[u] > rank[v])   swap(u, v);

         parent[u] = v;

       if (rank[u] == rank[v])  ++rank[v];

     }

};

時間複雜度:並集為O(lgn),查詢為O(lgn)

另一種優化方法是路徑壓縮優化


示例:判斷圖的連通性

示例:追蹤最大集合






相關文章