最小生成樹之prim算法
邊賦以權值的圖稱為網或帶權圖,帶權圖的生成樹也是帶權的,生成樹T各邊的權值總和稱為該樹的權。
最小生成樹(MST):權值最小的生成樹。
生成樹和最小生成樹的應用:要連通n個城市需要n-1條邊線路。可以把邊上的權值解釋為線路的造價。則最小生成樹表示使其造價最小的生成樹。
構造網的最小生成樹必須解決下面兩個問題:
1、盡可能選取權值小的邊,但不能構成回路;
2、選取n-1條恰當的邊以連通n個頂點;
MST性質:假設G=(V,E)是一個連通網,U是頂點V的一個非空子集。若(u,v)是一條具有最小權值的邊,其中u∈U,v∈V-U,則必存在一棵包含邊(u,v)的最小生成樹。
1.prim算法
基本思想:假設G=(V,E)是連通的,TE是G上最小生成樹中邊的集合。算法從U={u0}(u0∈V)、TE={}開始。重復執行下列操作:
在所有u∈U,v∈V-U的邊(u,v)∈E中找一條權值最小的邊(u0,v0)並入集合TE中,同時v0並入U,直到V=U為止。
此時,TE中必有n-1條邊,T=(V,TE)為G的最小生成樹。
Prim算法的核心:始終保持TE中的邊集構成一棵生成樹。
注意:prim算法適合稠密圖,其時間復雜度為O(n^2),其時間復雜度與邊得數目無關,而kruskal算法的時間復雜度為O(eloge)跟邊的數目有關,適合稀疏圖。
看了上面一大段文字是不是感覺有點暈啊,為了更好理解我在這里舉一個例子,示例如下:
(1)圖中有6個頂點v1-v6,每條邊的邊權值都在圖上;在進行prim算法時,我先隨意選擇一個頂點作為起始點,當然我們一般選擇v1作為起始點,好,現在我們設U集合為當前所找到最小生成樹里面的頂點,TE集合為所找到的邊,現在狀態如下:
U={v1}; TE={};
(2)現在查找一個頂點在U集合中,另一個頂點在V-U集合中的最小權值,如下圖,在紅線相交的線上找最小值。
通過圖中我們可以看到邊v1-v3的權值最小為1,那么將v3加入到U集合,(v1,v3)加入到TE,狀態如下:
U={v1,v3}; TE={(v1,v3)};
(3)繼續尋找,現在狀態為U={v1,v3}; TE={(v1,v3)};在與紅線相交的邊上查找最小值。
我們可以找到最小的權值為(v3,v6)=4,那么我們將v6加入到U集合,並將最小邊加入到TE集合,那么加入后狀態如下:
U={v1,v3,v6}; TE={(v1,v3),(v3,v6)}; 如此循環一下直到找到所有頂點為止。
(4)下圖像我們展示了全部的查找過程:
2.prim算法程序設計
(1)由於最小生成樹包含每個頂點,那么頂點的選中與否就可以直接用一個數組來標記used[max_vertexes];(我們這里直接使用程序代碼中的變量定義,這樣也易於理解);當選中一個數組的時候那么就標記,現在就有一個問題,怎么來選擇最小權值邊,注意這里最小權值邊是有限制的,邊的一個頂點一定在已選頂點中,另一個頂點當然就是在未選頂點集合中了。我最初的一個想法就是窮搜了,就是在一個集合中選擇一個頂點,來查找到另一個集合中的最小值,這樣雖然很易於理解,但是很明顯效率不是很高,在嚴蔚敏的《數據結構》上提供了一種比較好的方法來解決:設置兩個輔助數組lowcost[max_vertexes]和closeset[max_vertexes],lowcost[max_vertexes]數組記錄從U到V-U具有最小代價的邊。對於每個頂點v∈V-U,closedge[v], closeset[max_vertexes]記錄了該邊依附的在U中的頂點。
注意:我們在考慮兩個頂點無關聯的時候設為一個infinity 1000000最大值。
說了這么多,感覺有點羅嗦,還是發揚原來的風格舉一個例子來說明,示例如下:
過程如下表:頂點標號都比圖中的小1,比如v1為0,v2為1,這里首先選擇v1點。
Lowcost[0] |
Lowcost[1] |
Lowcost[2] |
Lowcost[3] |
Lowcost[4] |
Lowcost[5] |
U |
V-U |
|
closeset |
v1,infinity |
v1,6 |
v1,1 |
v1,5 |
v1,infinity |
v1,infinity |
v1 |
v1,v2,v3,v4,v5,v6 |
從這個表格可以看到依附到v1頂點的v3的Lowcost最小為1,那么選擇v3,選擇了之后我們必須要更新Lowcost數組的值,因為記錄從U到V-U具有最小代價的邊,加入之后就會改變。這里更新Lowcost和更新closeset數組可能有點難理解,
for (k=1;k<vcount;k++)
if (!used[k]&&(G[j][k]<lowcost[k]))
{ lowcost[k]=G[j][k];
closeset[k]=j; }
}
j為我們已經選出來的頂點,如果G[j][k]<lowcost[k],則意味着最小權值邊發生變化,更新該頂點的最小lowcost權值,依附的頂點肯定就是剛剛選出的頂點j,closeset[k]=j。
Lowcost[0] |
Lowcost[1] |
Lowcost[2] |
Lowcost[3] |
Lowcost[4] |
Lowcost[5] |
U |
V-U |
|
closeset |
v1,infinity |
v1,6 |
v1,1 |
v1,5 |
v3,6 |
v3,4 |
v1,v3 |
v1,v2,v4,v5,v6 |
這樣一直選擇下去直到選出所有的頂點。
(2)上面把查找最小權值的邊結束了,但是這里有一個問題,就是我們沒有存儲找到的邊,如果要求你輸出找到的邊那么這個程序就需要改進了,我們剛開始的時候選取的是v1作為第一個選擇的頂點,那我們設置一個father[]數組來記錄每個節點的父節點,當然v1的父節點肯定沒有,那么我們設置一個結束標志為-1,每次找到一個新的節點就將它的父節點設置為他依附的節點,這樣就可以准確的記錄邊得存儲了。
語法:prim(Graph G,int vcount,int father[]); |
|
參數: |
|
G: |
圖,用鄰接矩陣表示 |
vcount: |
表示圖的頂點個數 |
father[]: |
用來記錄每個節點的父節點 |
返回值: |
null |
注意: |
|
|
常數max_vertexes為圖最大節點數 |
|
常數infinity為無窮大 |
數組存儲從0開始 |
|
如果下面的源程序有錯請參照測試程序。 |
|
源程序: |
|
|
#define infinity 1000000 int closeset[max_vertexes],used[max_vertexes]; int min; /* 最短距離初始化為其他節點到1號節點的距離 */ /* 標記所有節點的依附點皆為默認的1號節點 */
/* vcount個節點至少需要vcount-1條邊構成最小生成樹 */ min = infinity; /* 找滿足條件的最小權值邊的節點k */ /* 邊權值較小且不在生成樹中 */ { min = lowcost[k]; j=k; } /* 發現更小的權值 */ lowcost[k]=G[j][k];/*更新最小權值*/ } |
測試程序:
測試用例:
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
5 6 6
4 6 2
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define infinity 1000000
#define max_vertexes 6
typedef int Graph[max_vertexes][max_vertexes];
void prim(Graph G,int vcount,int father[])
{
int i,j,k;
int lowcost[max_vertexes];
int closeset[max_vertexes],used[max_vertexes];
int min;
for (i=0;i<vcount;i++)
{
/* 最短距離初始化為其他節點到1號節點的距離 */
lowcost[i]=G[0][i];
/* 標記所有節點的依附點皆為默認的1號節點 */
closeset[i]=0;
used[i]=0;
father[i]=-1;
}
used[0]=1; /*第一個節點是在s集合里的*/
/* vcount個節點至少需要vcount-1條邊構成最小生成樹 */
for (i=1;i<=vcount-1;i++)
{
j=0;
min = infinity;
/* 找滿足條件的最小權值邊的節點k */
for (k=1;k<vcount;k++)
/* 邊權值較小且不在生成樹中 */
if ((!used[k])&&(lowcost[k]<min))
{
min = lowcost[k];
j=k;
}
father[j]=closeset[j];
printf("%d %d\n",j+1,closeset[j]+1);//打印邊
used[j]=1;;//把第j個頂點並入了U中
for (k=1;k<vcount;k++)
/* 發現更小的權值 */
if (!used[k]&&(G[j][k]<lowcost[k]))
{
lowcost[k]=G[j][k];/*更新最小權值*/
closeset[k]=j;;/*記錄新的依附點*/
}
}
}
int main()
{
FILE *fr;
int i,j,weight;
Graph G;
int fatheer[max_vertexes];
for(i=0; i<max_vertexes; i++)
for(j=0; j<max_vertexes; j++)
G[i][j] = infinity;
fr = fopen("prim.txt","r");
if(!fr)
{
printf("fopen failed\n");
exit(1);
}
while(fscanf(fr,"%d%d%d", &i, &j, &weight) != EOF)
{
G[i-1][j-1] = weight;
G[j-1][i-1] = weight;
}
prim(G,max_vertexes,fatheer);
return 0;
}
程序結果:
3 1
6 3
4 6
2 3
5 2
請按任意鍵繼續. . .
最小生成樹之kruskal算法
1.kruskal算法
假設連通網N=(V,{E})。則令最小生成樹的初始狀態為只有n個頂點而無邊的非連通圖T=(V,{}),圖中每個頂點自成一個連通分量。在E中選擇最小代價的邊,若該邊依附的頂點落在T中不同的連通分量中,則將該邊加入到T中,否則舍去此邊而選擇下一條代價最小的邊,依次類推,直到T中所有頂點都在同一連通分量上為止。
示例如下:
圖中先將每個頂點看作獨立的子圖,然后查找最小權值邊,這條邊是有限制條件的,邊得兩個頂點必須不在同一個圖中,如上圖,第一個圖中找到最小權值邊為(v1,v3),且滿足限制條件,繼續查找到邊(v4,v6),(v2,v5),(v3,v6),當查找到最后一條邊時,僅僅只有(v2,v3)滿足限制條件,其他的如(v3,v4),(v1,v4)都在一個子圖里面,不滿足條件,至此已經找到最小生成樹的所有邊。
2.kruskal算法程序設計
由於我們要查找邊權值最小的邊,那么我們的第一步應該把邊權值排序,這樣就可以很快查找到權值最小的邊,為了簡化程序設計,我們不使用其他的數據結構,僅僅設立一個結構體數組來存儲邊,用一個標記數組來標記哪些邊已經被選擇(實際程序設計的時候沒有用到標記數組);
解決了邊得存儲和排序問題,現在是算法最關鍵的就是怎么判斷邊的兩個頂點不在一個子圖里面,一個簡單的辦法是設立一個輔助數組f[n],初始化如下:
void Initialize()
{
int i;
for(i=0; i<n;i++)
f[i] = i;
}
如此初始化是為了讓每個頂點各自為一個圖,如果找到一條邊(i,j)那么做如下標記:(i<j)
void Mark_same(int i, int j)
{
//找到i的父節點
while(f[i] != i)
{
i= f[i];
}
f[j] = i;//將j指向其父節點
}
上面的標記過程也給了我們怎么判斷兩個頂點是否在一個圖中找到了方法,即判斷其父節點是否相同,相同則是在一個圖里;
int Is_same(int i, int j)
{
//找到i的父節點
while(f[i] != i)
{
i= f[i];
}
//找到i的父節點
while(f[j] != j)
{
j= f[j];
}
return i == j ? 1 : 0;
}
注意:實際設計程序的時候不用標記已選邊,因為已選擇的邊會講端點集合合並為一個集合,從而在判斷是否為同一集合的時候就可以排除了。
測試用例:
Kruskal.txt
0 1 6
0 2 1
0 3 5
1 2 5
1 4 3
2 3 5
2 4 6
2 5 4
4 5 6
3 5 2
測試程序:
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
#define N 6//頂點數目
/* 定義邊(x,y),權為w */
typedef struct
{
int x,y;
int w;
}edge;
edge e[MAX];
/* father[x]表示x的父節點 */
int father[N];
/* 比較函數,按權值(相同則按x坐標)非降序排序 */
int cmp(const void *a, const void *b)
{
if ((*(edge *)a).w == (*(edge *)b).w)
{
return (*(edge *)a).x - (*(edge *)b).x;
}
return (*(edge *)a).w - (*(edge *)b).w;
}
/* 判斷集合是否相同 */
int Is_same(int i, int j)
{
//找到i的父節點
while(father[i] != i)
{
i = father[i];
}
//找到i的父節點
while(father[j] != j)
{
j = father[j];
}
return i == j ? 1 : 0;
}
/* 合並x,y所在的集合 */
void Mark_same(int i, int j)
{
int temp;
if(i > j)
{
temp = i;
i = j;
j = temp;
}
//找到i的父節點
while(father[i] != i)
{
i= father[i];
}
father[j] = i;//將j指向其父節點
}
//初始化 father數組
void Initialize()
{
int i;
for(i=0; i<N;i++)
father[i] = i;
}
/* 主函數 */
int main()
{
int i = 0,j, n;
int x, y;
FILE *fr;
fr = fopen("kruskal.txt","r");
if(!fr)
{
printf("fopen failed\n");
exit(1);
}
/* 讀取邊信息並初始化集合 */
while(fscanf(fr,"%d %d %d", &e[i].x, &e[i].y, &e[i].w) != EOF)
i++;
/* 將邊排序 */
qsort(e, i, sizeof(edge), cmp);
Initialize();
for (i = 0; i < N; i++)
{
if(!Is_same(e[i].x, e[i].y))
{
printf("%d %d\n",e[i].x+1, e[i].y+1);
Mark_same(e[i].x, e[i].y);
}
}
system("pause");
return 0;
}
程序結果:
1 3
4 6
2 5
3 6
1 4
2 3
請按任意鍵繼續. . .