資料結構值圖的最小生成樹
最小生成樹(最小連通網)
假設在n個城市之間建立通訊聯絡網,則連通n個城市只需要n-1條線路。這時自然會考慮這樣一個問題,如何在最節省經費的前提下建立這個通訊網。
在每兩個城市之間都可以設定一條線路,相應地都要付出一定的經濟代價。n個城市之間,最多可能設定n(n-1)/2條線路,那麼,如何在這些可能的線路中選擇n-1條,以使總的耗費最少呢?
可以用連通網來表示n個城市以及n個城市間可能設定的通訊線路,其中網的頂點表示城市,邊表示兩城市間的線路,賦予邊的權值表示相應的代價。如下圖所示:
對於n個頂點的連通網可以建立許多不同的生成樹,每一個生成樹都可以是一個通訊網。如下面這些圖所示(紅線部分):
現在,我們要選擇這樣一顆生成樹,也就是使總的耗費最少。這個問題就是構造連通網的最小代價生成樹的問題。一棵生成樹的的代價就是樹上各邊的代價之和。
構造最小生成樹可以有多種演算法。其中多數演算法利用了最小生成樹的下列一種簡稱MST的性質:假設N=(V,{E})是一個連通網,U是頂點集V的一個非空子集。若(u,v)是一條具有最小權值的邊,其中u∈U,v∈V-U,則必存在一顆包含邊(u,v)的最小生成樹。
普利姆(Prim)演算法和克魯斯卡爾(Kruskal)演算法是兩個利用MST性質構造最小生成樹的演算法。
首先介紹普利姆演算法。
基本思想:
1. 從圖 N = { V, E }中選擇某一頂點 u0進行標記,之後選擇與它關聯的具有最小權值的邊(u(u0, v),並將頂點 v 進行標記;
2.反覆在一個頂點被標記,而另一個頂點未被標記的各條邊中選擇權值最小的邊(u, v),並將未標記的頂點進行標記;
3.如此繼續下去,直到圖中的所有頂點都被標記為止。
演算法步驟:
1.從某一頂點 u0 出發, 使得U={u0}, TE={};
2.每次選擇一條邊,這條邊是所有(u, v)中權值最小的邊,且u∈U, v∈V-U。修改U和TE:
TE = TETE + { (u,v) }
U = U + { v };
3.當U≠V時,轉2;否則,結束。
下面我們假設以二維陣列表示網的連線矩陣,且令兩個頂點之間不存在的邊的權值為機內允許的最大值來實現普利姆演算法:
根據演算法思想我們知道實現演算法之前需要附設幾個輔助陣列,如下:
#define VNUM 9 // 頂點數
#define MV 65536 // 最大數值
int P[VNUM]; // 輔助陣列
int Cost[VNUM]; // 存放耗費成本,權值
int Mark[VNUM]; // 標記陣列
當然,前提是我們得有一個圖,手動生成如下:
// 鄰接矩陣定義圖 MV表示頂點間沒有變,不鄰接
int Matrix[VNUM][VNUM] =
{
{0, 10, MV, MV, MV, 11, MV, MV, MV},
{10, 0, 18, MV, MV, MV, 16, MV, 12},
{MV, 18, 0, 22, MV, MV, MV, MV, 8},
{MV, MV, 22, 0, 20, MV, MV, 16, 21},
{MV, MV, MV, 20, 0, 26, MV, 7, MV},
{11, MV, MV, MV, 26, 0, 17, MV, MV},
{MV, 16, MV, MV, MV, 17, 0, 19, MV},
{MV, MV, MV, 16, 7, MV, 19, 0, MV},
{MV, 12, 8, 21, MV, MV, MV, MV, 0},
};
普利姆演算法實現程式碼如下:
// 普利姆演算法
void Prim(int sv) // O(n*n)
{
int i = 0;
int j = 0;
// 引數合法性檢查
if( (0 <= sv) && (sv < VNUM) )
{
// 初始化
for(i=0; i<VNUM; i++)
{
Cost[i] = Matrix[sv][i]; // 初始化當前頂點與其他頂點的權值
P[i] = sv;
Mark[i] = 0; // 全未標記
}
// 標記初始頂點sv
Mark[sv] = 1;
// 遍歷圖
for(i=0; i<VNUM; i++)
{
int min = MV;
int index = -1;
// 遍歷所有未標記的頂點
for(j=0; j<VNUM; j++)
{
// 選擇與當前標記頂點關聯的具有最小權值的未標記的頂點
if( !Mark[j] && (Cost[j] < min) )
{
min = Cost[j]; // 最小權值
index = j; // 與當前標記頂點關聯的具有最小權值的邊的頂點
}
}
// 將找到的頂點標記,列印邊及權值
if( index > -1 )
{
Mark[index] = 1; // 標記頂點
printf("(%d, %d, %d)\n", P[index], index, Cost[index]);
}
// 遍歷所有其他未標記的頂點
for(j=0; j<VNUM; j++)
{
// 更新與標記頂點關聯的所有具有較小權值的邊
// 找到所有與以標記頂點相鄰的未標記頂點的邊,留著選擇最小邊
// 重新選擇具有最小代價的邊
if( !Mark[j] && (Matrix[index][j] < Cost[j]) )
{
Cost[j] = Matrix[index][j];
P[j] = index;
}
}
}
}
}
分析演算法,假設網中有n個頂點,則第一個進行初始化的迴圈語句的頻度為n,第二個迴圈語句的頻度也為n。其中有兩個內迴圈:其一是在Cost[]中求最小值,其二是重新選擇具有最小代價的邊,頻度都為n。由此,普利姆演算法的時間複雜度為O(n2),與網中的邊數無關,因此適用於求邊稠密的網的最小生成樹。
既然最小連通網是以邊的權值之和為最終目標,那麼是不是可以直接選擇邊,而不是通過頂點來選擇邊呢?
克魯斯卡爾演算法就是直接選擇邊來求圖的最小生成樹的。
基本思想:
1.對於 n 個頂點的圖 G = { V, E };
2. 構造一個只有 n 個頂點, 沒有邊的圖 G’ = { V, ∅ };
3. 在 E 中選擇一條具有最小權值的邊, 若該邊的兩個頂點不構成迴路,則將此邊加入到 T 中; 否則將此邊捨去,重新選擇一條權值最小的邊;
4. 如此重複下去,直到所有頂點都連通為止。
演算法實現步驟:
1. 定義邊結構體;
2. 定義邊集陣列並排序;
定義輔助陣列 P[n],其中 n 為頂點數目,P[n]的用於記錄邊頂點的首尾連線關係,例如:
演算法核心思想:
1.遍歷 edges 陣列中的每個元素
1.1.通過 P 陣列查詢 begin 頂點的最終連線點 v1
1.2.通過 P 陣列查詢 end 頂點的最終連線點 v2
1.2.1.v1 != v2 則:當前邊為最小連通網中的邊,記錄連線關係P[v1] = v2;
1.2.2. v1 == v2 則: 產生迴路,捨棄當前邊。
克魯斯卡爾演算法實現參見:克魯斯卡爾演算法