圖的最小生成樹:Prim演算法和Kruskal演算法
阿新 • • 發佈:2019-01-04
1. 圖的最小生成樹
生成樹的定義:如果連通圖G的一個子圖是一棵包含G的所有頂點的樹,則該子圖稱為G的生成樹。
生成樹是連通圖的包含圖中的所有頂點的極小連通子圖。它並不唯一,從不同的頂點出發進行遍歷,可以得到不同的生成樹。
其中,權值最小的樹就是最小生成樹。
關於最小生成樹最經典的應用模型就是城市通訊線路網最小造價的問題:網路G表示n個城市之間的通訊線路(其中頂點表示城市,邊表示兩個城市之間的通訊線路,邊上的權值表示線路的長度或造價),通過求該網路的最小生成樹找到求解通訊線路總造價最小的最佳方案。
求圖的最小生成樹主要有兩種經典演算法:
- 普里姆(Prim)演算法
時間複雜度為O(n2),適合於求邊稠密的最小生成樹。 - 克魯斯卡爾(Kruskal)演算法
2. 普里姆(Prim)演算法
2.1 演算法思想
取圖中任意一個頂點V作為生成樹的根,之後若要往生成樹上新增頂點W,則在頂點V和W之間必定存在一條邊。並且該邊的權值在所有連通頂點V和W之間的邊中取值最小。
2.2 演算法實現
- 輸入:一個加權連通圖,其中頂點集合為V,邊集合為E;
- 初始化:Vnew = {x},其中x為集合V中的任一節點(起始點),Enew = {}為空;
重複下列操作,直到Vnew = V:
a.在集合E中選取權值最小的邊<u, v>
,其中u為集合Vnew中的元素,而v不在Vnew集合當中,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);
b.將v加入集合Vnew中,將<u, v>
輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。
完整實現的示意圖如下:
程式碼實現為:
int edge[1005][1005];//用鄰接矩陣表示的圖
int book[1005];//已確定的節點集合
int dis[1005];//最短路徑
int num=0;//節點的總個數
int prim(int s) {
int pos, min;//pos記錄每次確定下來要加入集合book的那個節點,min表示當前這輪確定的最短路徑
int MST = 0;//累加最短路徑,表示最短路徑和
book[s] = 1;//首先,將節點s放入集合book
pos = s;
for (int i = 1; i <= num; i++) //初始化dis
dis[i] = edge[pos][i];
//執行n-1次
for (int i = 2; i <= num; i++) {
min = INT_MAX;
for (int j = 1; j <= num; j++) {
if (book[j]==0 && min > dis[j]) {
min = dis[j];
pos = j;
}
}
book[pos] = 1;//確定選擇出離當前節點最近的那個節點
MST += min;//加上這條最短路徑
for (int j = 1; j <= num; j++) //嘗試更新dis
if (book[j]==0 && dis[j] > edge[pos][j])//更新條件:j點未訪問,加入新點後已訪問集合到j的距離變小了
dis[j] = edge[pos][j];
}
return MST;
}
int main() {
int n;
int i,j;
while(cin>>n) {
for(int i=1;i<=n;i++) {
book[i]=0;
dis[i]=0;
for(int j=1;j<=n;j++) {
edge[i][j]=0;
}
}
num=n;
for(i=1;i<=n;i++)//輸入鄰接矩陣(圖)
for(j=1;j<=n;j++)
cin>>edge[i][j];
int ans = prim(1);
cout << ans << endl;
}
return 0;
}
可以發現,prim演算法的結構和dj演算法很相似,都是每次確定一個節點,然後更新dis陣列,只不過更新的時候略有不同。
3. 克魯斯克爾(Kruskal)演算法
3.1 演算法思想
有n個頂點的最小生成樹含有n-1條邊。
按權值遞增次序選擇合適的邊來構造最小生成樹。先把所有的頂點包括在生成樹中,將圖中的邊按權值遞增的順序依次選取,要保證選取的邊不使生成樹中產生迴路,將選取的邊加到生成樹中,直至有n-1條邊為止。
3.2 演算法實現
- 記Graph中有v個頂點,e條邊;
- 新建圖Graphnew,Graphnew中擁有原圖中的v個頂點,但沒有邊;
- 將原圖Graph中所有e條邊按權值從小到大排序;
- 迴圈:從權值最小的邊開始,判斷並新增每條邊,直至添加了n-1條邊:
if 這條邊連線的兩個節點於圖Graphnew中不在同一個連通分量中(即不構成迴路)
新增這條邊到圖Graphnew中
程式碼實現為:
typedef struct
{
int begin;
int end;
int weight;
}Edge; //定義邊集結構
void sort_edges(Edge& edge,Edge& edge2) //交換大小,為後面排序所用
{
Edge edge_temp;
edge_temp.begin = edge.begin;
edge_temp.end = edge.end;
edge_temp.weight = edge.weight;
edge.begin = edge2.begin;
edge.end = edge2.end;
edge.weight = edge2.weight;
edge2.begin = edge_temp.begin;
edge2.end = edge_temp.end;
edge2.weight = edge_temp.weight;
}
void turn_to_edges(MGraph &G,Edge* edges) //講鄰接矩陣轉換成邊集陣列
{
int k=0;
for(int i=0;i<G.vexnum;++i)
{
for(int j=0;j<G.vexnum;++j)
{
if( (G.arcs[i][j] != MAX) && (i<j))
{
edges[k].begin = i;
edges[k].end=j;
edges[k].weight=G.arcs[i][j];
k++;
}
}
}
k=G.edgenum-1;
for(int i=0;i<G.edgenum;++i) //講轉換的邊集陣列按從小到大的順序排列
{
int weight=0,index=0;
for(int j= 0;j<k;++j)
{
if(edges[j].weight>weight)
{
weight = edges[j].weight;
index = j;
}
}
sort_edges(edges[index],edges[k]);
--k;
}
}
int find(int* parent,int num) //查詢已新增進樹中的頂點最後尾部索引
{
while(parent[num])
num = parent[num];
return num;
}
void minTree_kruskal(MGraph &G)
{
int *parent = (int*)malloc(sizeof(int)*G.vexnum);
Edge *edges = (Edge*)malloc(sizeof(Edge)*G.edgenum);
turn_to_edges(G,edges);
for(int i=0;i<G.vexnum;++i)
parent[i]=0;
for(int i=0;i<G.edgenum;++i)
{
int n = find(parent,edges[i].begin);
int m = find(parent,edges[i].end);
if(m!=n) //若相等,則新增此邊會形成環
{
parent[n]=m;
printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges[i].weight);
}
}
}