圖——最小生成樹
圖——最小生成樹
1. 基本概念
在一個連通無向圖G=(V, E)中,對於其中的每條邊(u,v)∈E,賦予其權重w(u, v),則最小生成樹問題就是要在G中找到一個連通圖G中所有頂點的無環子集T⊆E,使得這個子集中所有邊的權重之和w(T) = 最小。顯然,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] =
,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;
}