最小生成樹及其構造方法
ps:好久沒寫新文章了,來到了廣州見習,沒有網路,也不知道這學校怎麼想的!!接下來進入正題
何謂最小生成樹
對於一個帶權連通無向圖G中的不同生成樹,各棵樹的邊上的權值之和可能不同,邊上的權值之和最小的樹稱之為該圖得最小生成樹
構造方法
求圖得最小生成樹的倆個演算法,即普利姆演算法和克魯斯卡爾演算法
普利姆演算法(Prim)
在瞭解普利姆演算法之前,先想想我們最開始的目的是什麼?俗氣一點說就是畫路線圖,而且這個路線必須是最短的。
思路
先用抽象一點的語言描述,就是現在我們先拿出圖中的第一個頂點(其實可以是任意頂點),然後把這個頂點放進集合A中,然後將這個頂點與集合外的頂點連成的線的權值做比較,把那個能連權值最小的線的頂點也放進集合裡面。然後再把集合裡面的倆個頂點與集合外的頂點繼續比較,重複此步驟。可以參考:
第一步:從①開始,①進集合,用與集合外所有頂點能構成的邊中找最小權值的一條邊
①——②權6
①——③權1 -> 取①——③邊
①——④權5
第二步:③進集合,①,③與②,④,⑤,⑥構成的最小邊為
①——④權5
③——⑥權4 -> 取③——⑥邊
第三步:⑥進集合,①,③,⑥與②,④,⑤構成的各最小邊
①——②權6
③——②權5
⑥——④權2 -> 取⑥——④邊
第四步:④進集合,①,③,⑥,④與②,⑤構成的各最小邊
①——②權6
③——②權5 -> 取③——②邊
⑥——⑤權6
第四步:②進集合,①,③,⑥,②,④與⑤構成的各最小邊
②——⑤權3 -> 取②——⑤邊
沒錯上面的說法很好,但實際中的程式碼跟上面的思想是一樣,但就有點難理解,我們換一種說法,從程式碼的角度來分析。
假如我們現在拿到手的是一個鄰接矩陣儲存方法的圖(假設這個圖有n個頂點)
1.那麼首先我們先例項化一個數組A,一個數組B。先這麼規定,陣列A儲存的是n-1條邊的權值。那麼另外一個數組就是儲存邊的起始頂點。舉個例子A[1]=3,B[1]=4,代表的就是第一個頂點所連線的最短的邊的權值是3,然後這條最短邊是和第4個頂點連線而成的。
2.那麼有了這倆個數組,事情就變得好辦很多了。B[i]=k代表的就是i與k連線可以形成最小權值的邊,而A[i]陣列就代表i頂點與這個k(k=B[i])連線的邊的那個最小權值。這裡有點繞口。
從實際例子來講思路比較好,先看看下圖:
3.
for(i=0;i<g.n;i++)
{
lowcost[i] = g.edges[0][i];
closest[i] = 0;
}
此時lowcost陣列的意義就是各個頂點到0頂點的距離,closest陣列的原理就是當前lowcost中的各個數值是由哪倆個頂點連起來的,可見初始化過程使得closest的每個值都為0,也就是lowcost中的每條邊都是和0頂點連起來的.
4.接下來,取出lowcost中最小的邊,如下:
min = INF;
//先遍歷出lowcost裡面最小的拿出來
for(j=0;j<g.n;j++)
{
if(lowcost[j]!=0 && lowcost[j]<min)
{
min = lowcost[j];
k = j;
}
}
printf("邊(%d,%d)權為:%d\n",closest[k],k,min);
lowcost[k] = 0;
取出的這條邊就是頂點0與頂點k連線而成的。先用我們的腦子想一下,取出這條最短邊之後我們應該做些什麼事?
5.沒錯,取出第二條最短的邊,我們需要把能和頂點0和頂點k連線的那些邊取出來然後進行比較儲存在lowcost陣列中。
這也就跟我們第一個抽象化的思路一樣,頂點0和頂點k在集合裡面,與集合外面的頂點進行比較。
那麼我們該怎麼做這個比較的操作呢?
for(j=0;j<g.n;j++)
{
if(g.edges[k][j]!=0 && lowcost[j]>g.edges[k][j])
{5
lowcost[j] = g.edges[k][j];
closest[j] = k;
}
}
g.edges[k][j]就是頂點j到頂點k的距離,lowcost[j]就是頂點j到頂點0的目前最短距離(在第一次大迴圈中,由於closest[j]都等於0,所以都是與頂點0的距離),拿這倆個數值做比較,得出一個更小的距離儲存到lowcost中。
那麼整個演算法就是:
程式碼
#define INF 32767;
void Prim(MGraph g,int v)
{
int lowcost[MAXV];
int min;
//就是對應的元素下標的頂點的連線的上一個頂點
int closest[MAXV];
int i,k;
for(i=0;i<g.n;i++)
{
lowcost[i] = g.edges[v][i];
closest[i] = v;
}
for(i=1;i<g.n;i++)
{
min = INF;
//先遍歷出lowcost裡面最小的拿出來
for(j=0;j<g.n;j++)
{
if(lowcost[j]!=0 && lowcost[j]<min)
{
min = lowcost[j];
k = j;
}
}
printf("邊(%d,%d)權為:%d\n",closest[k],k,min);
lowcost[k] = 0;
//接下來做的就是把集合裡的倆個頂點跟集合外的頂點組成的邊的權值做比較
for(j=0;j<g.n;j++)
{
if(g.edges[k][j]!=0 && lowcost[j]>g.edges[k][j])
{
lowcost[j] = g.edges[k][j];
closest[j] = k;
}
}
}
}
總結
對於普利姆演算法來說,只要理解lowcost陣列和closest陣列,基本大概思路就已經清晰了
克魯斯卡爾演算法(Kruskal)
克魯斯卡爾演算法的思想就是先把所有邊取出來,然後按從小到大排序,每次取出一條邊,判斷是否形成迴路,如果是就丟棄,否則這條邊就是我們想要的權值最小的邊。
思路
核心-怎麼樣去判斷迴路
在這裡需要簡單地說一下思路。上面已經說了,我們有一個數組是按權值從小到大儲存邊的(這裡先不要去考慮怎麼儲存邊),然後我們每次拿出來一條邊,這時候就需要判斷這條邊會不會跟我們前面已選擇的邊形成迴路,因此,每次取出邊的時候,我們需要把它的“原始頂點”存放到vset陣列中,那麼怎麼存放呢?
假設我們現在有邊ab,bc,cd都被取出來。那麼這三條邊就屬於一個連通分量,也就是到時是連在一起的。那麼ad這倆個頂點就不能連線起來了,很明顯會構成一個迴路,計算機應該怎麼判斷呢。可以這樣規定,ab連在一起了,所以b的parent是a,bc連在一起了,所以c的parent是b,cd連在一起了所以d的parent是c。那麼我們可以通過下面的程式碼來判斷是否構成迴路:
int Find(int *parent,int f)
{
while(parnet[f]>0)
f = parent[f]
return f;
}
上面的程式碼實現了查詢一個連通分量的其實頂點,通過上面的程式碼,我們可以知道d
的“原始頂點”是a,a的原始頂點也是a,所以相等,並不能連線。
程式碼
所以整個演算法就是:
int Find(int *parent,int f)
{
while(parnet[f]>0)
f = parent[f]
return f;
}
Typedef struct
{
int begin;
int end;
int weight;
}Edge;
void Kruskal(MGraph g)
{
Edge E[MaxSize];
int vset[MaxSize];
int i,k,j;
for(i=0;i<g.n;i++)
for(j=0;j<g.n;j++)
if(g.edges[i][j]!=0 && g.edges[i][j]!=INF)
{
E[k].begin = i;
E[k].end = j;
E[k].weight = g.edges[i][j];
k++;
}
InsertSort(E,g.e);//排序
for(i=0;i<g.n;i++) vset[i]=0;
//要區n-1條邊出來
k = 1;
//每次取出一條邊,所以要有一個元素記住現在取到第幾條邊了
j = 0;
while(k<g.n)
{
u1 = E[j].begin;
u2 = E[j].end;
sn1 = Find(vset,u1);
sn2 = Find(vset,u2);
if(sn1!=sn2)
{
vset[sn2] = sn1;
printf();
k++;
}
j++;
}
}