1042 Quiz#1 互斥集合 (Disjoint Sets)

C++ 實習測試: 互斥集合 (disjoint-sets) 類別製作
時間: 60分鐘 (10:20 上傳時間截止)

令集合 S 為整數 0 到 n-1 的集合,「互斥集合 (disjoint-sets)」資料結構 (或稱為「不相交集合」, 也稱為 union-find 資料結構, 或是 merge-find 資料結構) 的定義如下:

一個 disjoint-sets 資料結構描述集合 S 的一個分割 (partition),也就是描述一組 S 的子集合,其中任何兩個子集合的交集都是空集合,這一組子集合的聯集是 S,例如下面的 DS1 以及 DS2

令 S = {0,1, 2, 3, 4, 5, 6, 7}
DS1 = {0}, {1, 2}, {3, 6, 7}, {4}, {5}
DS2 = {0, 1, 2, 5}, {3, 6, 7}, {4}

一個 disjoint-sets 資料結構中每一個子集合都會任意指定一個代表元素, 例如上面每一個子集合中都有一個元素標示為紅色

disjoint-sets 抽象資料型態支援兩個主要的運算方法:

    1. find(i): 找到元素 i 的子集合,回傳該子集合的代表元素
    2. union(i, j): 將包含元素 i 的子集合與包含元素 j 的子集合聯集起來

以上面的 DS1 為例:

  • 執行 find(0) 會得到 0, 執行 find(6) 會得到 7, 同時 find(1)==find(2)

  • 執行 union(1,4) 會使得 DS1 成為 {0, 3, 6, 7}, {1, 2}, {4}, {5}, 其中子集合 {0, 3, 6, 7} 的代表元素需要為這四個元素中的一個, 一般都是原來的兩個子集合 {0} 和 {3, 6, 7} 的代表元素中的一個

  • 執行 union(0,2), 再執行 union(1,5) 會使得 DS1 成為等效於 DS2 的 disjoint-sets (每一個子集合的代表元素不見得和 DS2 一樣)

此外還有一個 makeset() 的運算, 將 disjoint-sets 資料結構初始化為 n 個含有單一元素的子集合, 例如

DS3 = {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}

我們可以用 陣列 簡單地實作 「互斥集合」資料結構, 用樹狀架構來代表各個子集合, 例如 DS2 = {0, 1, 2, 5}, {3, 6, 7}, {4} 表示為下圖之三個樹狀架構

用樹根節點作為這個子集合的代表元素, 下圖的整數陣列中每一個元素 parent[i] 中紀錄的是節點 i 的父節點, 如果是根節點的話 parent[i] 記錄的是 i 自己, 例如 DS2 = {0, 1, 2, 5}, {3, 6, 7}, {4} 以下列整數陣列表示

注意: 用這種表示方法來表示子集合並不是唯一的,同一個子集合可以有不同的樹狀架構,不考慮效率的情況下這個實作可以滿足下列 main() 函式中的基本測試

#include <cstdlib>
#include <cassert>
#include <iostream>
#include <iomanip>
using namespace std;

void makeSets(int nNodes, int parent[]);
int  findSets(int i, int nNodes, const int parent[]);
void mergeTrees(int i, int j, int nNodes, int parent[]);
void unionSets(int i, int j, int nNodes, int parent[]);
int  equalSets(int nNodes1, const int parent1[], int nNodes2, const int parent2[]);
void printSets(int nNodes, const int parent[]);

int main()
{
    const int nNodes=8;
    int parent[nNodes];
    int i;
    const int p1[8] = {0,1,2,3,4,5,6,7};
    const int p2[8] = {0,0,2,3,4,5,6,7};
    const int p3[8] = {0,0,3,3,4,5,6,7};
    const int p4[8] = {0,0,3,3,4,4,6,7};
    const int p5[8] = {0,0,3,3,5,5,5,7};
    const int p6[8] = {0,0,3,3,6,4,6,6};
    const int p7[8] = {0,0,3,4,6,4,6,6};
    const int p8[8] = {7,0,3,4,6,4,6,6};
    makeSets(nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p1));
    unionSets(0, 1, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p2));
    unionSets(2, 3, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p3));
    unionSets(4, 5, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p4));
    unionSets(4, 6, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p5));
    unionSets(4, 7, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p6));
    unionSets(3, 7, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p7));
    unionSets(2, 1, nNodes, parent);
    assert(equalSets(nNodes, parent, nNodes, p8));
    printSets(nNodes, parent);
    system("pause");
    return 0;
}

void printSets(int nNodes, const int parent[])
{
    int i, j;
    
    int *printed = (int *) malloc(nNodes*sizeof(int));
    for (i=0; i<nNodes; i++) printed[i] = 0;
    for (i=0; i<nNodes; i++)
        if (!printed[i])
        {
            if (i==0)
                cout << "{";
            else
                cout << ", {";
            cout << i;
            for (j=i+1; j<nNodes; j++)
                if (!printed[j] && 
                    findSets(i, nNodes, parent)==findSets(j, nNodes, parent))
                {
                    cout << ", " << j;
                    printed[j] = 1;
                }
            cout << "}";
        }
    cout << endl;
    free(printed);
}


void makeSets(int nNodes, int parent[])
{
    int i;
    for (i=0; i<nNodes; i++)
        parent[i] = i;
}

int findSets(int i, int nNodes, const int parent[])
{
    if ((i<0)||(i>=nNodes)) return -1;
    if (i == parent[i]) return i;
    return findSets(parent[i], nNodes, parent);
}

void mergeTrees(int i, int j, int nNodes, int parent[])
{
    if ((i<0)||(i>=nNodes)) return;
    if ((j<0)||(j>=nNodes)) return;
    parent[i] = j;
}

void unionSets(int i, int j, int nNodes, int parent[])
{
    if ((i<0)||(i>=nNodes)) return;
    if ((j<0)||(j>=nNodes)) return;
    mergeTrees(findSets(i, nNodes, parent), 
               findSets(j, nNodes, parent),
               nNodes, parent);
}

int equalSets(int nNodes1, const int parent1[], int nNodes2, const int parent2[])
{
//     請實作一個測試兩個 disjoint-sets 是否相等的函式, 相等請回傳 1, 不等請回傳 0
//     首先檢查兩個 disjoint-sets 的元素個數是否相等
//     測試兩個集合 S1 與 S2 是否相等的基本方法是檢查 S1 中的每一個元素是否在 S2 裡, 
//     同時反過來檢查 S2 中的每一個元素是否在 S1 裡, 請運用 findSet() 函式來測試, 
//     請考慮兩個集合的代表元素可能會不一樣
    return 1;
}

考試時間: 60分鐘 (10:20 上傳時間截止)

  1. 請製作一個新的方案, 讓 Visual Studio 替方案建立目錄 (請勾選「為方案建立目錄」), 方案名稱為 DisjointSets, 設定專案名稱為 C_Version, 將上面程式拷貝進 main.cpp
  2. 完成上面的 equalSets() 函式 (如果你真的寫不出這個函式的話, 時間不多, 請先維持 return 1;, 繼續進行下面的步驟)
  3. 測試程式功能 (所有的 assert 應該都是成功的, 程式輸出 {0, 1, 2, 3, 4, 5, 6, 7})
  4. 請替目前方案新增一個專案, 名稱是 CPP_Version (請將這個專案設為啟始專案)
  5. 製作一個 C++ 版本的 DisjointSets 類別, 類別裡有兩個資料成員 m_nNodes, m_parent, 其中 m_parent 請設計成 100 個元素的整數陣列
  6. DisjointSets 類別裡需要有六個成員函式 makeSets(), findSets(), mergeTrees(), unionSets(), equalSets(), print()
    • 請注意設計成員函式的存取權限 private/public
    • 請移除各個函式中不需要的參數 (成員函式是一個 DisjointSets 的物件回應它收到的訊息所執行的函式, 不需要再傳遞我們設計成資料成員的那些東西了)
    • makeSets() 函式請額外傳入一個整數值 nNodes 來設定 m_nNodes
    • makeSets() 函式請傳入一個整數指標 parent (代表一個整數陣列) 來設定 m_parent 陣列裡的內容, 這個整數指標 parent 請設計為有預設值的參數, 呼叫時如果沒有給第二個參數的話, 預設值為 0, 此時 makeSets() 函式內部自動建立為 nNodes 個單一元素的不相交子集合
    • 函式參數或是函式本身可以使用 const 的地方請儘量使用,
    • 如果使用 參考變數 可以簡化的地方請使用參考變數的語法
  7. 類別內請加入一個靜態的 unitTest() 函式, 內容包含上述 main.cpp 裡面的所有測試, main() 函式中請呼叫 unitTest() 來測試你定義的 DisjointSets 類別界面 (測試時請在方案總管或是類別檢視中選擇 CPP_Version 專案, 建置選單裡才會出現建置 CPP_Version 的選項)
將所完成的 project (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 以 zip/rar/7zip 程式將整個資料匣壓縮起來, 在「考試作業」繳交區選擇 Labtest1上傳

後續練習

這個 DisjointSets 物件可以用來

  • 尋找某一個二元關係 (binary relationship) 的 equivalence class, 例如某甲和某乙住在同一行政區, 某乙和某丙也住在同一行政區, 則某甲和某丙一定住在同一行政區
  • 作為實作 Kruskal Minimal Spanning Tree 演算法的工具
  • 作為產生隨機迷宮演算法的工具: 範例執行程式
    # rows? 10
    # cols? 20
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      | | |*| |*|*| | | | | | | |*| |*|*| |*|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |*| |*| | | | |*| |*| |*|*| | | | |*|*| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | | | |*|*| |*| | | | | |*|*| |*| | | | |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | |*| | | | | | |*| |*|*| | |*| | |*| |*|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |*| |*| |*| |*| | | | | |*| | |*| | | | |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | | | | |*| | |*| |*| |*| | |*| | |*|*| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | |*| |*|*| |*| |*| | |*| |*| |*| | |*| |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | | | |*| | | |*|*|*| | | | | | |*| | |*|
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | |*| |*| |*| |*| |*|*| |*| |*| | |*| | |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | | | | | |*| | | | | | |*| | |*| | |*|  
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

演算法大致描述如下:

Initialize DisjointSets S with n=nRows*nCols elements and 
an nRows x nCols maze of '*' except maze[0][0] and 
maze[nRows-1][nCols-1] are ' '
while (S.findset(0) != S.findset(n-1)) { choose randomly a cell maze[ir][ic] that contains a '*' and
satisfies the "route constraints" maze[ir][ic] = ' '
if a direct neighbour cell of maze[ir][ic] is ' ',
then union with this cell }
the following 4 cases do not satisfy the "route constraints"
+-+-+-+  +-+-+-+  +-+-+-+  +-+-+-+
| | |*|  |*| | |  |*|*|*|  |*|*|*|
+-+-+-+  +-+-+-+  +-+-+-+  +-+-+-+
| |x|*|  |*|x| |  | |x|*|  |*|x| |
+-+-+-+  +-+-+-+  +-+-+-+  +-+-+-+
|*|*|*|  |*|*|*|  | | |*|  |*| | |
+-+-+-+  +-+-+-+  +-+-+-+  +-+-+-+
x denotes the chosen cell maze[ir][ic]. In these cases, if the cell is cleared and considered a portion of road, the road would become very wide and not like a real maze.

C++ 物件導向程式設計課程 首頁

製作日期: 03/21/2016 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon