1. 程式人生 > >圖——最小生成樹

圖——最小生成樹

圖——最小生成樹

1. 基本概念

在一個連通無向圖G=(V, E)中,對於其中的每條邊(u,v)∈E,賦予其權重w(u, v),則最小生成樹問題就是要在G中找到一個連通圖G中所有頂點的無環子集T⊆E,使得這個子集中所有邊的權重之和w(T) = ( u ,

v ) T w ( u ,
v ) \displaystyle\sum_{(u,v)∈T} w(u, v) 最小。顯然,T必然是一棵樹,這樣的樹通常稱為圖G的生成樹

2. 最小生成樹演算法

通常來說,要解決最小生成樹問題,通常採用兩種演算法:Prim演算法Kruskal演算法。先假設要求一個連通無向圖G=(V, E)的最小生成樹T,且以其中的一個頂點V1為T的根結點。下面就分別對這兩種演算法進行介紹。

2.1 Prim演算法

2.1.1 基本思想

Prim演算法構建最小生成樹的過程是:先構建一棵只包含根結點V1的樹A,然後每次在連線樹A結點和圖G中樹A以外的結點的所有邊中,選取一條權重最小的邊加入樹A,直至樹A覆蓋圖G中的所有結點。

注意:在這個演算法中,關鍵點在於在連線樹A結點和圖G中樹A以外的結點的所有邊中選取權重最小的邊。在演算法實現過程中,要記錄G中每個結點i到樹A中結點的最小距離minidistance[i],以及與之相連的樹A中的那個結點miniadj[i]。minidistance[i]開始都初始化為無窮大,miniadj[i]都初始化為該頂點自己;每將一個結點j加入了樹A,首先令minidistance[j]=0,然後遍歷圖G中所有不在樹A中的結點,看看往樹A中加入了結點j後,這些結點到樹A中結點的最小距離會不會變小,如果變小則進行調整。例如,對於結點k,它在圖G中,但不在樹A中,且結點k與結點j相連,該邊的權重為w(k, j)。在未將結點j加入樹A前,結點k到樹A中結點的最小距離為minidistance[k];將結點j加入樹A後,如果minidistance[k] > w(k, j),那麼結點k到樹A中結點的最小距離就是結點k到結點j的最小距離,所以要將minidistance[k]調整為w(k, j),且miniadj[k]為j。

例子
有一個無向連通圖G,它有5個頂點,7條邊,如下圖所示。
在這裡插入圖片描述
以結點A最為根結點,利用Prim演算法來生成該圖的最小生成樹T的過程如下:
(1)開始T為空,初始化minidistance[A] = minidistance[B] = minidistance[C] = minidistance[D] = minidistance[E] = {\infty} ,miniadj[A] = A,miniadj[B] = B, miniadj[C] = C,miniadj[D] = D,miniadj[E] = E;

(2)將結點A加入T,這時minidistance[A] = 0,miniadj[A] = A, minidistance[B] = 6,miniadj[B] = A ,minidistance[C] = 1,miniadj[C] = A , minidistance[D] = 4,miniadj[D] = A , minidistance[E] = 4,miniadj[E] = A 。

(3)此時結點B,C,D,E都不在樹T中,在連線樹T結點和圖G中樹A以外的結點的所有邊中,權重最小的邊是minidistance[C],且miniadj[C]=A,所以將結點C和結點C與結點A連成的邊加入樹T。此時,minidistance[A] = 0,miniadj[A] = A, minidistance[B] = 6,miniadj[B] = A ,minidistance[C] = 0,miniadj[C] = A , minidistance[D] = 4,miniadj[D] = A , minidistance[E] = 4,miniadj[E] = A 。

(4)此時結點B,D,E都不在樹T中,在連線樹T結點和圖G中樹A以外的結點的所有邊中,權重最小的邊是minidistance[D]和minidistance[E],從中隨便選一個,此處選結點D,且miniadj[D]=A,所以將結點D和結點D與結點A連成的邊加入樹T。此時,minidistance[A] = 0,miniadj[A] = A, minidistance[B] = 2,miniadj[B] = D ,minidistance[C] = 0,miniadj[C] = A , minidistance[D] = 0,miniadj[D] = A , minidistance[E] = 4,miniadj[E] = A 。

(5)此時結點B,E都不在樹T中,在連線樹T結點和圖G中樹A以外的結點的所有邊中,權重最小的邊是minidistance[B],且miniadj[B]=D,所以將結點B和結點B與結點D連成的邊加入樹T。此時,minidistance[A] = 0,miniadj[A] = A, minidistance[B] = 0,miniadj[B] = D ,minidistance[C] = 0,miniadj[C] = A , minidistance[D] = 0,miniadj[D] = A , minidistance[E] = 2,miniadj[E] = B。

(6)此時只有結點E都不在樹T中,因此在連線樹T結點和圖G中樹A以外的結點的所有邊中,權重最小的邊是minidistance[E],且miniadj[E]=B,所以將結點E和結點E與結點B連成的邊加入樹T,此時最小生成樹構成,如下圖所示:
在這裡插入圖片描述

2.1.2 程式碼實現

#define Maximum 1000
#define Biggest 100000000

typedef struct EdgeListNode{
    int adjId;
    int weight;
    EdgeListNode* next;
};

typedef struct VertexListNode{
    int data;
    EdgeListNode* firstadj;
};

typedef struct GraphAdjList{
    int vertexnumber;
    int edgenumber;
    VertexListNode vertextlist[Maximum];
};

typedef struct MiniTreeEdge {
    int s;
    int e;
    int weight;
    MiniTreeEdge *next;
};

typedef struct MiniTree {  //最小生成樹
    MiniTreeEdge *head;  //指向最小生成樹的根節點
    int vertextnumber;
};

void MiniSpanTree_Prim(GraphAdjList g, MiniTree tree, int start_node) {
    tree.head = NULL;
    int *distance = (int*)malloc(sizeof(int) * g.vertexnumber + 2);
    int *miniadj = (int*)malloc(sizeof(int) * g.vertexnumber + 2);
    int i, j, k, lastnode, thisnode;
    lastnode = start_node;
    for(i=1; i<=g.vertexnumber; i++) {
        distance[i] = Biggest;
        miniadj[i] = i;
    }

    distance[start_node] = 0;
    tree.vertextnumber = 1;

    while(tree.vertextnumber < g.vertexnumber) {
        EdgeListNode *temp = g.vertextlist[lastnode].firstadj;
        while(temp != NULL) {
            j = temp->adjId;
            if(distance[j] && distance[j]>temp->weight) {
                distance[j] = temp->weight;
                miniadj[j] = lastnode;
            }
            temp = temp->next;
        }

        k = Biggest;
        for(i=1; i<=g.vertexnumber; i++) {
            if(distance[i] && k>distance[i]) {
                k = distance[i];
                thisnode = i;
            }
        }

        MiniTreeEdge *temp1 = (MiniTreeEdge*)malloc(sizeof(MiniTreeEdge));
        temp1->e = thisnode; //新加入的結點
        temp1->s = miniadj[thisnode]; //最小生成樹中與新加入結點相連的結點
        temp1->weight = k; //新加入的邊的權重
        temp1->next = NULL;
        temp1->next = tree.head;
        tree.head = temp1;
        distance[thisnode] = 0;

        lastnode = thisnode;
        tree.vertextnumber++;
    }

	//列印最小生成樹
    MiniTreeEdge *e = tree.head;
    while(e != NULL) {
        cout<<e->s<<" -> "<<e->e<<"  :  "<<e->weight<<endl;
        e = e->next;
    }

}

2.1.3 測試

測試的圖為:
在這裡插入圖片描述

int main() {
    GraphAdjList g;
    g.edgenumber = 5;
    g.vertexnumber = 4;
    int i, j, k;
    EdgeListNode *temp;

    for(i=1; i<=4; i++) {
        g.vertextlist[i].data = i;
        g.vertextlist[i].firstadj = NULL;
    }

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 2; temp->weight = 2; temp->next = g.vertextlist[1].firstadj;
    g.vertextlist[1].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 4; temp->weight = 8; temp->next = g.vertextlist[1].firstadj;
    g.vertextlist[1].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 1; temp->weight = 2; temp->next = g.vertextlist[2].firstadj;
    g.vertextlist[2].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 3; temp->weight = 3; temp->next = g.vertextlist[2].firstadj;
    g.vertextlist[2].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 4; temp->weight = 1; temp->next = g.vertextlist[2].firstadj;
    g.vertextlist[2].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 2; temp->weight = 3; temp->next = g.vertextlist[3].firstadj;
    g.vertextlist[3].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 4; temp->weight = 5; temp->next = g.vertextlist[3].firstadj;
    g.vertextlist[3].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 1; temp->weight = 8; temp->next = g.vertextlist[4].firstadj;
    g.vertextlist[4].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 2; temp->weight = 1; temp->next = g.vertextlist[4].firstadj;
    g.vertextlist[4].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 3; temp->weight = 5; temp->next = g.vertextlist[4].firstadj;
    g.vertextlist[4].firstadj = temp;

    MiniTree tree;
    MiniSpanTree_Prim(g, tree, 1);
    MiniTreeEdge *e = tree.head;
    while(e != NULL) {
        cout<<e->s<<" -> "<<e->e<<"  :  "<<e->weight<<endl;
        e = e->next;
    }

    return 0;
}

2.2 Kruskal演算法

2.2.1 基本思想

假設現在要求無向連通圖G=(V, E)的最小生成樹T,Kruskal演算法的思想是令T的初始狀態為|V|個結點而無邊的非連通圖,T中的每個頂點自成一個連通分量。接著,每次從圖G中所有兩個端點落在不同連通分量的邊中,選取權重最小的那條,將該邊加入T中,如此往復,直至T中所有頂點都在同一個連通分量上。

注意
此處的關鍵點有兩個:
(1)在生成最小生成樹前,要對圖中的所有邊進行排序;
(2)如何判斷一條邊的兩個端點是否落在不同的連通分量上:
這裡可以用一個數組parent來記錄每個端點在其所在連通圖中的父端點(例如parent[i]表示端點i的父端點),在一個連通分量中,總有一個端點的父端點是它自己(把它看成是一棵樹的根節點)。
1)開始的時候初始化該陣列為parent[i]=i(因為每個端點所在的連通圖只有自己);
2)每次要判斷一條邊(u, v)的兩個端點是否在在不同的連通分量上,就找端點u和端點v在它們所在的連通圖中的最底層的父端點,具體的方法是遞迴地找端點k的父端點,直至某個端點的父端點等於它自己:

int find_parent(int node, int *parent) { //找端點node的最底層父端點
    while(parent[node] != node) {
        node = parent[node];
    }
    return node;
}

3)假設n=find_parent(u, parent),m=find_parent(v, parent),那麼就要對它們進行判斷:
如果n==m,說明結點u和結點v在一個連通分量上,因此不能將其加入T;
如果n!=m,說明結點u和結點v不在一個連通分量上,這時可以將(u,v)加入T,且令parent[n]=m(或parent[m]=n)。

例子
仍然以上述這個無向連通圖G為例,它有5個頂點,7條邊,如下圖所示。
在這裡插入圖片描述
利用Prim演算法來生成該圖的最小生成樹T的過程如下:
(1)先對圖中的所有邊按照權重遞增的順序排序:
(A, C, 1) , (B, D, 2) , (B, E, 2) , (A, D , 4) , (A, E , 4) , (A, B, 6) , (C, D, 8).
對每個端點的parent進行初始化:
parent[A] = A, parent[B] = B, parent[C] = C, parent[D] = D, parent[E] = E.

(2)從權重最短的邊開始遍歷:
1)(A, C, 1),因為parent[A] != parent[C],所以將邊(A, C)加入樹T,且令parent[parent[C]]=parent[A]=A.此時,parent[A] = A, parent[B] = B, parent[C] = A, parent[D] = D, parent[E] = E,此時T中有1條邊。

2)(B, D, 2),因為parent[B] != parent[D],將邊(B, D)加入樹T,且令parent[parent[D]]=parent[B]=B.此時,parent[A] = A, parent[B] = B, parent[C] = A, parent[D] = B, parent[E] = E,此時T中有2條邊.

3)(B, E, 2),因為parent[B] != parent[E],將邊(B, E)加入樹T,且令parent[parent[E]]=parent[B]=B.此時,parent[A] = A, parent[B] = B, parent[C] = A, parent[D] = B, parent[E] = B,此時T中有3條邊

4)(A, D, 2),因為parent[A] != parent[D],將邊(A, D)加入樹T,且令parent[parent[D]]=parent[A]=A.此時,parent[A] = A, parent[B] = A, parent[C] = A, parent[D] = B, parent[E] = E,此時T中有4條邊,正好等於端點數減一,因此樹T生成。

2.2.2 具體實現

#define Maximum 1000
#define Biggest 100000000

typedef struct EdgeListNode{
    int adjId;
    int weight;
    EdgeListNode* next;
};

typedef struct VertexListNode{
    int data;
    EdgeListNode* firstadj;
};

typedef struct GraphAdjList{
    int vertexnumber;
    int edgenumber;
    VertexListNode vertextlist[Maximum];
};

typedef struct MiniTreeEdge {
    int s;
    int e;
    int weight;
    MiniTreeEdge *next;
};

typedef struct MiniTree {
    MiniTreeEdge *head;
    int edgenumber;
};

typedef struct EdgeArrayData {
    int l;
    int r;
    int weight;
};

bool compare(EdgeArrayData a, EdgeArrayData b) {
    return a.weight < b.weight;
}

int find_parent(int node, int *parent) {
    while(parent[node] != node) {
        node = parent[node];
    }
    return node;
}

void  MiniSpanTree_Kruskal(GraphAdjList g, MiniTree *tree) {
    int i, j, k, edge_index, *parent;
    MiniTreeEdge *e;
    EdgeArrayData *edge = (EdgeArrayData*)malloc(sizeof(EdgeArrayData)*(g.edgenumber+2));
    parent = (int*)malloc(sizeof(int)*(g.vertexnumber+2));
    tree = (MiniTree*)malloc(sizeof(MiniTree));
    EdgeListNode *v;

	//將圖中的每條邊儲存在edge裡
    edge_index = 0;
    for(i=1; i<=g.vertexnumber; i++) {
        v = g.vertextlist[i].firstadj;
        parent[i] = i;
        while(v != NULL) {
            if(v->adjId > i) {  //為了避免將一條邊存兩次
                edge[edge_index].l = i;
                edge[edge_index].r = v->adjId;
                edge[edge_index].weight = v->weight;
                edge_index++;
            }
            v = v->next;
        }
    }
    sort(edge, edge+edge_index, compare); //將邊按權重從小到大排序
    
    tree->edgenumber = 0;
    tree->head = NULL;
    for(i=0; i<edge_index; i++) {
        j = find_parent(edge[i].l, parent); 
        k = find_parent(edge[i].r, parent);
        if(j != k) {
            parent[j] = k;
            e = (MiniTreeEdge*)malloc(sizeof(MiniTreeEdge));
            e->s = edge[i].l;
            e->e = edge[i].r;
            e->weight = edge[i].weight;
            e->next = tree->head;
            tree->head = e;
            tree->edgenumber++;
        }
        if(tree->edgenumber == g.vertexnumber - 1) {
            break;
        }
    }

    MiniTreeEdge *ee = tree->head;
    while(ee != NULL) {
        cout<<ee->s<<" -> "<<ee->e<<"  :  "<<ee->weight<<endl;
        ee = ee->next;
    }
}

2.2.3 測試

測試的圖為:
在這裡插入圖片描述


int main() {
    GraphAdjList g;
    g.edgenumber = 5;
    g.vertexnumber = 4;
    int i, j, k;
    EdgeListNode *temp;

    for(i=1; i<=4; i++) {
        g.vertextlist[i].data = i;
        g.vertextlist[i].firstadj = NULL;
    }

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 2; temp->weight = 2; temp->next = g.vertextlist[1].firstadj;
    g.vertextlist[1].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 4; temp->weight = 8; temp->next = g.vertextlist[1].firstadj;
    g.vertextlist[1].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 1; temp->weight = 2; temp->next = g.vertextlist[2].firstadj;
    g.vertextlist[2].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 3; temp->weight = 3; temp->next = g.vertextlist[2].firstadj;
    g.vertextlist[2].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 4; temp->weight = 1; temp->next = g.vertextlist[2].firstadj;
    g.vertextlist[2].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 2; temp->weight = 3; temp->next = g.vertextlist[3].firstadj;
    g.vertextlist[3].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 4; temp->weight = 5; temp->next = g.vertextlist[3].firstadj;
    g.vertextlist[3].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 1; temp->weight = 8; temp->next = g.vertextlist[4].firstadj;
    g.vertextlist[4].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 2; temp->weight = 1; temp->next = g.vertextlist[4].firstadj;
    g.vertextlist[4].firstadj = temp;

    temp = (EdgeListNode*)malloc(sizeof(EdgeListNode));
    temp->adjId = 3; temp->weight = 5; temp->next = g.vertextlist[4].firstadj;
    g.vertextlist[4].firstadj = temp;

    MiniTree *tree;
    MiniSpanTree_Kruskal(g, tree);
    MiniTreeEdge *e = tree->head;
    while(e != NULL) {
        cout<<e->s<<" -> "<<e->e<<"  :  "<<e->weight<<endl;
        e = e->next;
    }

    return 0;
}