數據結構--最小生成樹詳解

前言
A wise man changes his mind,a fool never.
Name:Willam
Time:2017/3/1

1、什麼是最小生成樹

現在假設有一個很實際的問題:我們要在n個城市中建立一個通信網絡,則連通這n個城市需要佈置n-1一條通信線路,這個時候我們需要考慮如何在成本最低的情況下建立這個通信網?
於是我們就可以引入連通圖來解決我們遇到的問題,n個城市就是圖上的n個頂點,然後,邊表示兩個城市的通信線路,每條邊上的權重就是我們搭建這條線路所需要的成本,所以現在我們有n個頂點的連通網可以建立不同的生成樹,每一顆生成樹都可以作爲一個通信網,當我們構造這個連通網所花的成本最小時,搭建該連通網的生成樹,就稱爲最小生成樹。

構造最小生成樹有很多算法,但是他們都是利用了最小生成樹的同一種性質:MST性質(假設N=(V,{E})是一個連通網,U是頂點集V的一個非空子集,如果(u,v)是一條具有最小權值的邊,其中u屬於U,v屬於V-U,則必定存在一顆包含邊(u,v)的最小生成樹),下面就介紹兩種使用MST性質生成最小生成樹的算法:普里姆算法和克魯斯卡爾算法。

2、普里姆算法—Prim算法

算法思路:
首先就是從圖中的一個起點a開始,把a加入U集合,然後,尋找從與a有關聯的邊中,權重最小的那條邊並且該邊的終點b在頂點集合:(V-U)中,我們也把b加入到集合U中,並且輸出邊(a,b)的信息,這樣我們的集合U就有:{a,b},然後,我們尋找與a關聯和b關聯的邊中,權重最小的那條邊並且該邊的終點在集合:(V-U)中,我們把c加入到集合U中,並且輸出對應的那條邊的信息,這樣我們的集合U就有:{a,b,c}這三個元素了,一次類推,直到所有頂點都加入到了集合U。

下面我們對下面這幅圖求其最小生成樹:

這裏寫圖片描述

假設我們從頂點v1開始,所以我們可以發現(v1,v3)邊的權重最小,所以第一個輸出的邊就是:v1—v3=1:
這裏寫圖片描述

然後,我們要從v1和v3作爲起點的邊中尋找權重最小的邊,首先了(v1,v3)已經訪問過了,所以我們從其他邊中尋找,發現(v3,v6)這條邊最小,所以輸出邊就是:v3—-v6=4
這裏寫圖片描述

然後,我們要從v1、v3、v6這三個點相關聯的邊中尋找一條權重最小的邊,我們可以發現邊(v6,v4)權重最小,所以輸出邊就是:v6—-v4=2.
這裏寫圖片描述

然後,我們就從v1、v3、v6、v4這四個頂點相關聯的邊中尋找權重最小的邊,發現邊(v3,v2)的權重最小,所以輸出邊:v3—–v2=5
這裏寫圖片描述

然後,我們就從v1、v3、v6、v4,v2這2五個頂點相關聯的邊中尋找權重最小的邊,發現邊(v2,v5)的權重最小,所以輸出邊:v2—–v5=3
這裏寫圖片描述

最後,我們發現六個點都已經加入到集合U了,我們的最小生成樹建立完成。

3、普里姆算法—代碼實現

(1)採用的是鄰接矩陣的方式存儲圖,代碼如下

#include<iostream>
#include<string>
#include<vector>
using  namespace std;

//首先是使用鄰接矩陣完成Prim算法
struct Graph {
    int vexnum;  //頂點個數
    int edge;   //邊的條數
    int ** arc; //鄰接矩陣
    string *information; //記錄每個頂點名稱
};

//創建圖
void createGraph(Graph & g) {
    cout << "請輸入頂點數:輸入邊的條數" << endl;
    cin >> g.vexnum;
    cin >> g.edge;  //輸入邊的條數

    g.information = new string[g.vexnum];
    g.arc = new int*[g.vexnum];
    int i = 0;

    //開闢空間的同時,進行名稱的初始化
    for (i = 0; i < g.vexnum; i++) {
        g.arc[i] = new int[g.vexnum];
        g.information[i]="v"+ std::to_string(i+1);//對每個頂點進行命名
        for (int k = 0; k < g.vexnum; k++) {
            g.arc[i][k] = INT_MAX;          //初始化我們的鄰接矩陣
        }
    }

    cout << "請輸入每條邊之間的頂點編號(頂點編號從1開始),以及該邊的權重:" << endl;
    for (i = 0; i < g.edge; i++) {
        int start;
        int end;
        cin >> start;   //輸入每條邊的起點
        cin >> end;     //輸入每條邊的終點
        int weight;
        cin >> weight;
        g.arc[start-1][end-1]=weight;//無向圖的邊是相反的
        g.arc[end-1][start-1] = weight;
    }
}

//打印圖
void print(Graph g) {
    int i;
    for (i = 0; i < g.vexnum; i++) {
        //cout << g.information[i] << " ";
        for (int j = 0; j < g.vexnum; j++) {
            if (g.arc[i][j] == INT_MAX)
                cout << "∞" << " ";
            else
            cout << g.arc[i][j] << " ";
        }
        cout << endl;
    }
}

//作爲記錄邊的信息,這些邊都是達到end的所有邊中,權重最小的那個
struct Assis_array {
    int start; //邊的終點
    int end;  //邊的起點
    int weight;  //邊的權重
};
//進行prim算法實現,使用的鄰接矩陣的方法實現。
void Prim(Graph g,int begin) {

    //close_edge這個數組記錄到達某個頂點的各個邊中的權重最大的那個邊
    Assis_array *close_edge=new Assis_array[g.vexnum];

    int j;

    //進行close_edge的初始化,更加開始起點進行初始化
    for (j = 0; j < g.vexnum; j++) {
        if (j != begin - 1) {
            close_edge[j].start = begin-1;
            close_edge[j].end = j;
            close_edge[j].weight = g.arc[begin - 1][j];
        }
    }
    //把起點的close_edge中的值設置爲-1,代表已經加入到集合U了
    close_edge[begin - 1].weight = -1;
    //訪問剩下的頂點,並加入依次加入到集合U
    for (j = 1; j < g.vexnum; j++) {

        int min = INT_MAX;
        int k;
        int index;
        //尋找數組close_edge中權重最小的那個邊
        for (k = 0; k < g.vexnum; k++) {
            if (close_edge[k].weight != -1) {  
                if (close_edge[k].weight < min) {
                    min = close_edge[k].weight;
                    index = k;
                }
            }
        }
        //將權重最小的那條邊的終點也加入到集合U
        close_edge[index].weight = -1;
        //輸出對應的邊的信息
        cout << g.information[close_edge[index].start] 
            << "-----" 
            << g.information[close_edge[index].end]
            << "="
            <<g.arc[close_edge[index].start][close_edge[index].end]
            <<endl;

        //更新我們的close_edge數組。
        for (k = 0; k < g.vexnum; k++) {
            if (g.arc[close_edge[index].end][k] <close_edge[k].weight) {
                close_edge[k].weight = g.arc[close_edge[index].end][k];
                close_edge[k].start = close_edge[index].end;
                close_edge[k].end = k;
            }
        }
    }
}



int main()
{
    Graph g;
    createGraph(g);//基本都是無向網圖,所以我們只實現了無向網圖
    print(g);
    Prim(g, 1);
    system("pause");
    return 0;
}

輸入:

6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 5 6
3 6 4
4 3 5
4 6 2
5 6 6

輸出:
這裏寫圖片描述

時間複雜度的分析:
其中我們建立鄰接矩陣需要的時間複雜度爲:O(n*n),然後,我們Prim函數中生成最小生成樹的時間複雜度爲:O(n*n).

(2)採用的是鄰接表的方式存儲圖,代碼如下

#include<iostream>
#include<string>
using  namespace std;
//表結點
struct ArcNode {
    int adjvex;      //某條邊指向的那個頂點的位置(一般是數組的下標)。
    ArcNode * next;  //指向下一個表結點
    int weight;      //邊的權重
};
//頭結點
struct Vnode {
    ArcNode * firstarc;  //第一個和該頂點依附的邊 的信息
    string data;       //記錄該頂點的信息。
};

struct Graph_List {
    int vexnum;     //頂點個數
    int edge;       //邊的條數
    Vnode * node;  //頂點表
};

//創建圖,是一個重載函數
void createGraph(Graph_List &g) {
    cout << "請輸入頂點數:輸入頂點邊的個數:" << endl;
    cin >> g.vexnum;
    cin >> g.edge;
    g.node = new Vnode[g.vexnum];
    int i;
    for (i = 0; i < g.vexnum; i++) {
        g.node[i].data = "v" + std::to_string(i + 1);  //對每個頂點進行命名
        g.node[i].firstarc = NULL;//初始化每個頂點的依附表結點
    }

    cout << "請輸入每條邊之間的頂點編號(頂點編號從1開始),以及該邊的權重:" << endl;
    for (i = 0; i < g.edge; i++) {
        int start;
        int end;
        cin >> start;   //輸入每條邊的起點
        cin >> end;     //輸入每條邊的終點
        int weight;
        cin >> weight;

        ArcNode * next = new ArcNode;
        next->adjvex = end - 1;
        next->next = NULL;
        next->weight = weight;
        //如果第一個依附的邊爲空
        if (g.node[start - 1].firstarc == NULL) {
            g.node[start - 1].firstarc = next;
        }
        else {
            ArcNode * temp; //臨時表結點
            temp = g.node[start - 1].firstarc;
            while (temp->next) {//找到表結點中start-1這個結點的鏈表的最後一個頂點
                temp = temp->next;
            }
            temp->next = next;  //在該鏈表的尾部插入一個結點


        }
        //因爲無向圖邊是雙向的
        ArcNode * next_2 = new ArcNode;
        next_2->adjvex = start - 1;
        next_2->weight = weight;
        next_2->next = NULL;

        //如果第一個依附的邊爲空
        if (g.node[end - 1].firstarc == NULL) {
            g.node[end - 1].firstarc = next_2;
        }
        else {
            ArcNode * temp; //臨時表結點
            temp = g.node[end - 1].firstarc;
            while (temp->next) {//找到表結點中start-1這個結點的鏈表的最後一個頂點
                temp = temp->next;
            }
            temp->next = next_2;  //在該鏈表的尾部插入一個結點


        }



    }
}

void print(Graph_List g) {
    cout<<"圖的鄰接表:"<<endl;
    for (int i = 0; i < g.vexnum; i++) {
        cout << g.node[i].data << " ";
        ArcNode * next;
        next = g.node[i].firstarc;
        while (next) {
            cout << "("<< g.node[i].data <<","<<g.node[next->adjvex].data<<")="<<next->weight << " ";
            next = next->next;
        }
        cout << "^" << endl;
    }
}


////作爲記錄邊的信息,這些邊都是達到end的所有邊中,權重最小的那個
struct Assis_array {
    int start; //邊的終點
    int end;  //邊的起點
    int weight;  //邊的權重
};

void Prim(Graph_List g, int begin) {
    cout << "圖的最小生成樹:" << endl;
    //close_edge這個數組記錄到達某個頂點的各個邊中的權重最大的那個邊
    Assis_array *close_edge=new Assis_array[g.vexnum];
    int j;
    for (j = 0; j < g.vexnum; j++) {
        close_edge[j].weight = INT_MAX;
    }
    ArcNode * arc = g.node[begin - 1].firstarc;

    while (arc) {
        close_edge[arc->adjvex].end = arc->adjvex;
        close_edge[arc->adjvex].start = begin - 1;
        close_edge[arc->adjvex].weight = arc->weight;
        arc = arc->next;
    }
    //把起點的close_edge中的值設置爲-1,代表已經加入到集合U了
    close_edge[begin - 1].weight = -1;
    //訪問剩下的頂點,並加入依次加入到集合U
    for (j = 1; j < g.vexnum; j++) {
        int min = INT_MAX;
        int k;
        int index;
        //尋找數組close_edge中權重最小的那個邊
        for (k = 0; k < g.vexnum; k++) {
            if (close_edge[k].weight != -1) {
                if (close_edge[k].weight < min) {
                    min = close_edge[k].weight;
                    index = k;
                }
            }
        }

        //輸出對應的邊的信息
        cout << g.node[close_edge[index].start].data
            << "-----"
            << g.node[close_edge[index].end].data
            << "="
            << close_edge[index].weight
            <<endl;
        //將權重最小的那條邊的終點也加入到集合U
        close_edge[index].weight = -1;
        //更新我們的close_edge數組。            
        ArcNode * temp = g.node[close_edge[index].end].firstarc;
        while (temp) {
            if (close_edge[temp->adjvex].weight > temp->weight) {
                close_edge[temp->adjvex].weight = temp->weight;
                close_edge[temp->adjvex].start = index;
                close_edge[temp->adjvex].end = temp->adjvex;
            }
            temp = temp->next;
        }
    }

}
int main()
{
    Graph_List g;
    createGraph(g);
    print(g);
    Prim(g, 1);
    system("pause");
    return 0;

輸入:

6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 5 6
3 6 4
4 3 5
4 6 2
5 6 6

輸出:
這裏寫圖片描述

時間複雜分析:
在建立圖的時候的時間複雜爲:O(n+e),在執行Prim算法的時間複雜還是:O(n*n),總體來說還是鄰接表的效率會比較高,因爲雖然Prim算法的時間複雜度相同,但是鄰接矩陣的那個常係數是比鄰接表大的。

另外,Prim算法的時間複雜度都是和邊無關的,都是O(n*n),所以它適合用於邊稠密的網建立最小生成樹。但是了,我們即將介紹的克魯斯卡算法恰恰相反,它的時間複雜度爲:O(eloge),其中e爲邊的條數,因此它相對Prim算法而言,更適用於邊稀疏的網。

4、克魯斯卡算法

算法思路:
(1)將圖中的所有邊都去掉。
(2)將邊按權值從小到大的順序添加到圖中,保證添加的過程中不會形成環
(3)重複上一步直到連接所有頂點,此時就生成了最小生成樹。這是一種貪心策略。

這裏同樣我們給出一個和Prim算法講解中同樣的例子,模擬克魯斯卡算法生成最小生成樹的詳細的過程:

首先完整的圖如下圖:
這裏寫圖片描述

然後,我們需要從這些邊中找出權重最小的那條邊,可以發現邊(v1,v3)這條邊的權重是最小的,所以我們輸出邊:v1—-v3=1
這裏寫圖片描述

然後,我們需要在剩餘的邊中,再次尋找一條權重最小的邊,可以發現邊(v4,v6)這條邊的權重最小,所以輸出邊:v4—v6=2
這裏寫圖片描述

然後,我們再次從剩餘邊中尋找權重最小的邊,發現邊(v2,v5)的權重最小,所以可以輸出邊:v2—-v5=3,
這裏寫圖片描述

然後,我們使用同樣的方式找出了權重最小的邊:(v3,v6),所以我們輸出邊:v3—-v6=4
這裏寫圖片描述

好了,現在我們還需要找出最後一條邊就可以構造出一顆最小生成樹,但是這個時候我們有三個選擇:(v1,V4),(v2,v3),(v3,v4),這三條邊的權重都是5,首先我們如果選(v1,v4)的話,得到的圖如下:
這裏寫圖片描述
我們發現,這肯定是不符合我們算法要求的,因爲它出現了一個環,所以我們再使用第二個(v2,v3)試試,得到圖形如下:
這裏寫圖片描述

我們發現,這個圖中沒有環出現,而且把所有的頂點都加入到了這顆樹上了,所以(v2,v3)就是我們所需要的邊,所以最後一個輸出的邊就是:v2—-v3=5

OK,到這裏,我們已經把克魯斯卡算法過了一遍,下面我們就用具體的代碼實現它:

5、克魯斯卡算法的代碼實現

/************************************************************/
/*                程序作者:Willam                          */
/*                程序完成時間:2017/3/3                    */
/*                有任何問題請聯繫:[email protected]       */
/************************************************************/
//@儘量寫出完美的程序


#include<iostream>
#include<algorithm>
#include<string>
using namespace std;

//檢驗輸入邊數和頂點數的值是否有效,可以自己推算爲啥:
//頂點數和邊數的關係是:((Vexnum*(Vexnum - 1)) / 2) < edge
bool check(int Vexnum,int edge) {
    if (Vexnum <= 0 || edge <= 0 || ((Vexnum*(Vexnum - 1)) / 2) < edge)
        return false;
    return true;
}

//判斷我們每次輸入的的邊的信息是否合法
//頂點從1開始編號
bool check_edge(int Vexnum, int start ,int end, int weight) {
    if (start<1 || end<1 || start>Vexnum || end>Vexnum || weight < 0) {
        return false;
    }
    return true;
}

//邊集結構,用於保存每條邊的信息
typedef struct edge_tag {
    bool visit; //判斷這條邊是否加入到了最小生成樹中
    int start;   //該邊的起點
    int end;   //該邊的終點
    int weight; //該邊的權重
}Edge;

//創建一個圖,但是圖是使用邊集結構來保存
void createGraph(Edge * &e,int Vexnum, int edge) {
    e = new Edge[edge];//爲每條邊集開闢空間
    int start = 0;
    int end = 0;
    int weight = 0;

    int i = 0;
    cout << "輸入每條邊的起點、終點和權重:" << endl;
    while (i != edge)
    {
        cin >> start >> end >> weight;
        while (!check_edge(Vexnum, start, end, weight)) {
            cout << "輸入的值不合法,請重新輸入每條邊的起點、終點和權重:" << endl;
            cin >> start >> end >> weight;
        }
        e[i].start = start;
        e[i].end = end;
        e[i].weight = weight;
        e[i].visit = false; //每條邊都還沒被初始化
        ++i;

    }
}

//我們需要對邊集進行排序,排序是按照每條邊的權重,從小到大排序。
int cmp(const void*  first, const void * second) {
    return ((Edge *)first)->weight - ((Edge *)second)->weight;
}

//好了,我們現在需要做的是通過一定的方式來判斷
//如果我們把當前的邊加入到生成樹中是否會有環出現。
//通過我們之前學習樹的知識,我們可以知道如果很多棵樹就組成一個森林,而且
//如果同一顆樹的兩個結點在連上一條邊,那麼就會出現環,
//所以我們就通過這個方式來判斷加入了一個新的邊後,是否會產生環,
//開始我們讓我們的圖的每個頂點都是一顆獨立的樹,通過不斷的組合,把這個森林變
//成來源於同一顆頂點的樹
//如果不理解,畫個圖就明白了,

//首先是找根節點的函數,
//其中parent代表頂點所在子樹的根結點
//child代表每個頂點孩子結點的個數
int find_root(int child, int * parent) {

    //此時已經找到了該頂點所在樹的根節點了
    if (parent[child] == child) {
        return child;
    }
    //往前遞歸,尋找它父親的所在子樹的根結點
    parent[child] = find_root(parent[child], parent);
    return parent[child];
}

//合併兩個子樹
bool union_tree(Edge  e, int * parent, int * child) {
    //先找出改邊所在子樹的根節點
    int root1;
    int root2;
    //記住我們頂點從1開始的,所以要減1
    root1 = find_root(e.start-1, parent);
    root2 = find_root(e.end-1, parent);
    //只有兩個頂點不在同一顆子樹上,纔可以把兩棵樹並未一顆樹
    if (root1 != root2) {
        //小樹合併到大樹中,看他們的孩子個數
        if (child[root1] > child[root2]) {
            parent[root2] = root1;
            //大樹的孩子數量是小樹的孩子數量加上
            //大樹的孩子數量在加上小樹根節點自己
            child[root1] += child[root2] + 1;
        }
        else {
            parent[root1] = root2;
            child[root2] += child[root1] + 1;
        }
        return true;
    }
    return false;
}

//克魯斯卡算法的實現
void Kruskal() {
    int Vexnum = 0;
    int edge = 0;
    cout << "請輸入圖的頂點數和邊數:" << endl;
    cin >> Vexnum >> edge;
    while (!check(Vexnum, edge)) {
        cout << "你輸入的圖的頂點數和邊數不合法,請重新輸入:" << endl;
        cin >> Vexnum >> edge;
    }

    //聲明一個邊集數組
    Edge * edge_tag;
    //輸入每條邊的信息
    createGraph(edge_tag, Vexnum, edge);

    int * parent = new int[Vexnum]; //記錄每個頂點所在子樹的根節點下標
    int * child = new int[Vexnum]; //記錄每個頂點爲根節點時,其有的孩子節點的個數
    int i;
    for (i = 0; i < Vexnum; i++) {
        parent[i] = i;
        child[i] = 0;
    }
    //對邊集數組進行排序,按照權重從小到達排序
    qsort(edge_tag, edge, sizeof(Edge), cmp);
    int count_vex; //記錄輸出的邊的條數

    count_vex = i = 0;
    while (i != edge) {
        //如果兩顆樹可以組合在一起,說明該邊是生成樹的一條邊
        if (union_tree(edge_tag[i], parent, child)) {
            cout << ("v" + std::to_string(edge_tag[i].start))
                << "-----"
                << ("v" + std::to_string(edge_tag[i].end))
                <<"="
                << edge_tag[i].weight
                << endl;
            edge_tag[i].visit = true;
            ++count_vex; //生成樹的邊加1
        }
        //這裏表示所有的邊都已經加入成功
        if (count_vex == Vexnum - 1) {
            break;
        }
        ++i;
    }

    if (count_vex != Vexnum - 1) {
        cout << "此圖爲非連通圖!無法構成最小生成樹。" << endl;
    }
    delete [] edge_tag;
    delete [] parent;
    delete [] child;
}

int main() {
    Kruskal();
    system("pause");
    return 0;
}

輸入:

6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 5 6
3 6 4
4 3 5
4 6 2
5 6 6

輸出:
這裏寫圖片描述

輸入:

7 9
1 2 20
1 5 1
2 3 6
2 4 4
3 7 2
4 6 12
4 7 8
5 6 15
6 7 10

輸出:
這裏寫圖片描述

發佈了70 篇原創文章 · 獲贊 1096 · 訪問量 101萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章