上一小節讨論了一些圖的基本概念。其中提到,如果有兩幅圖G=(V,E),G'=(V',E'),如果V'∈V,E'∈E,則稱G'是G的子圖。如果V'=V,且E'∈E(即頂點不少,邊少了幾條),那麼G'是G的生成子圖。特别的,如果生成子圖是一棵樹,即滿足定點數 = 邊樹+1,那麼這棵樹稱為生成樹。
最小生成樹的概念是基于“帶權圖”的。即圖中每條邊上都有特定的權值,這樣的圖又稱為網。最小生成樹指的是所有生成樹中,權值之和最小的樹。
當然,這個概念絕非數學家們自己胡思亂想出來的,它能對很多實際的問題模組化:比如有A,B,C,D,E,F,6個地方。我們需要修路讓他們之間可以互相連通。但是各個節點之間修路的花費可能不一樣,n個地方如果兩兩相通,一共有n(n-1)/2條路,我們想在其中找出n-1條,讓它們的花費最小。那麼如何選擇出這n-1條路徑呢?有兩種經典的算法Kruskal算法和Prim算法。
先看定義的一些基本的資料結構(總體上跟圖的差不多)
#include <stdio.h>
#include <limits.h>
#include <malloc.h>
#define MAX 10
//帶權圖的基本資料結構
typedef int** adjacentMatrix;
typedef struct WeightedGraph
{
adjacentMatrix matrix;
char* vertex;
int vertexNumber;
int arcNumber;
}wGraph;
//基本操作聲明
//初始化圖
wGraph initWgraph(int );
//銷毀圖
void destroyWgraph(wGraph*);
//增加一條邊
bool addArc(wGraph* ,char ,char ,int );
//删除一條邊
bool deleteArc(wGraph* ,char ,char );
//顯示鄰接矩陣
void printfWgraph(wGraph* );
//Kruskal算法相關函數
int findMiniWeightArc(wGraph* ,char*,char*);
void do_dfs(wGraph* ,int );
bool is_loop(wGraph* );
void Kruskal(wGraph* ,wGraph* );
Kruskal算法的基本思路如下:
1.将圖的邊集合按照權值大小排序(可選)。
2.從E中挑出一個權值最小的邊e,權值為we
3.将e加入到生成樹的邊的集合T中。
4.加入e之後判斷T是否形成回路
5.若T的邊數<n-1,則轉2,否則退出
程式如下:
wGraph initWgraph(int n)
{
wGraph g;
g.vertexNumber = n;
g.arcNumber = 0;
g.vertex = (char*)malloc(sizeof(char) * n);
char start = 'A';
for(int i = 0; i < n;++i)
g.vertex[i] = start + i;
g.matrix = (int**)malloc(sizeof(int*) * n);
for(int i = 0; i < n;++i)
g.matrix[i] = (int*)malloc(sizeof(int) * n);
//鄰接矩陣的初始為為最大值
for(int i = 0; i < n;++i)
for(int j = 0; j < n;++j)
g.matrix[i][j] = INT_MAX;
return g;
}
void destroyWgraph(wGraph* g)
{
free(g->vertex);
g->vertex = NULL;
for(int i = 0; i < g->vertexNumber;++i)
free(g->matrix[i]);
free(g->matrix);
g->matrix = NULL;
g->arcNumber = -1;
g->vertexNumber = -1;
}
bool addArc(wGraph* g,char vertex1,char vertex2,int weight)
{
int i = vertex1 - 'A';
int j = vertex2 - 'A';
if(i < 0 || i > g->vertexNumber || j < 0 || j > g->vertexNumber)
{
printf("vertexes does not exsist!\n");
return false;
}
else
{
g->matrix[i][j] = g->matrix[j][i] = weight;
g->arcNumber++;
return true;
}
}
bool deleteArc(wGraph* g,char vertex1,char vertex2)
{
int i = vertex1 - 'A';
int j = vertex2 - 'A';
if(i < 0 || i > g->vertexNumber || j < 0 || j > g->vertexNumber)
{
printf("vertexes does not exsist!\n");
return false;
}
if(INT_MAX == g->matrix[i][j] )
{
printf("there is no arc between vertexes!\n");
return false;
}
else
{
g->matrix[i][j] = INT_MAX;
g->matrix[j][i] = INT_MAX;
g->arcNumber--;
return true;
}
}
void printfWgraph(wGraph* g)
{
printf(" ");
for(int i = 0 ; i < g->vertexNumber;++i)
printf("%-4c ",g->vertex[i]);
for(int i = 0; i < g->vertexNumber;++i)
{
printf("\n");
printf("%-4c ",g->vertex[i]);
for(int j = 0; j < g->vertexNumber;++j)
{
if(INT_MAX == g->matrix[i][j] )
printf("NULL ");
else
printf("%-4d ",g->matrix[i][j]);
}
}
printf("\n");
}
//找完以後需要打上标記,否則下一次還會找他
int findMiniWeightArc(wGraph* g,char *vertex1,char *vertex2)
{
int miniWeight = INT_MAX;
int vex1;
int vex2;
for(int i = 0; i < g->vertexNumber;++i)
{
for(int j = 0; j< g->vertexNumber;++j)
{
// printf("測試邊%c%c,它的權值為%d\n",g->vertex[i],g->vertex[j],g->matrix[i][j]);
if(g->matrix[i][j] < miniWeight && g->matrix[j][i] < miniWeight && g->matrix[i][j] != -1 && g->matrix[j][i] != -1)
{
miniWeight = g->matrix[i][j];
vex1 = i;
vex2 = j;
}
}
}
//打上标記
g->matrix[vex1][vex2] = -1;
g->matrix[vex2][vex1] = -1;
*vertex1 = 'A' + vex1;
*vertex2 = 'A' + vex2;
return miniWeight;
}
//判斷加入以後是否形成回路
//visit1表明該節點是否被通路過
bool visit1[MAX];
//visit2表明該節點在這次搜尋中是否被通路過
bool visit2[MAX][MAX];
bool LOOP = false;
void do_dfs(wGraph* g,int i)
{
visit1[i] = true;
for(int j = 0; j < g->vertexNumber;++j)
{
//對目前節點,挑選可以通路的節點
if(INT_MAX != g->matrix[i][j])
{
//如果這個次通路為回溯,則跳過這個節點
if(visit2[i][j] == true)
continue;
else
{
if(visit1[j] == false)
{
visit2[i][j] = true;
visit2[j][i] = true;
do_dfs(g,j);
}
else
LOOP = true;
}
}
}
}
bool is_loop(wGraph* g)
{
//重置LOOP
LOOP = false;
//
for(int i = 0; i < g->vertexNumber;++i)
{
visit1[i] = false;
}
for(int i = 0; i < g->vertexNumber;++i)
{
for(int j = 0; j < g->vertexNumber;++j)
{
visit2[i][j] = false;
}
}
for(int i = 0; i < g->vertexNumber;++i)
{
if(false == visit1[i])
do_dfs(g,i);
}
return LOOP;
}
void Kruskal(wGraph* g,wGraph* tree)
{
//先初始化樹
*tree = initWgraph(g->vertexNumber);
while(tree->arcNumber < g->vertexNumber-1)
{
char vertex1;
char vertex2;
int weight = findMiniWeightArc(g,&vertex1,&vertex2);
addArc(tree,vertex1,vertex2,weight);
if(is_loop(tree) == true)
{
printf("被删除的邊為:%c,%c\n",vertex1,vertex2);
deleteArc(tree,vertex1,vertex2);
}
else
printf("挑出來的邊為:%c,%c\n",vertex1,vertex2);
// printfWgraph(tree);
}
}
再看Prim算法:
1.設頂點集合為V,從V中任取一個頂點,加入U中,并将這個節點從V中删去。
2.在U與V構成的所有邊中,選擇一個權值最小的,加入樹的邊集合E
3.将邊E的另一個節點vx也加入集合U中。
4.若V不為空,則轉2
//Prim算法相關的函數
void selectMiniWeightArc(wGraph* g,bool* u,bool* v,int* indexV,int* indexU);
void Prim(wGraph* g,wGraph* t,char Start);
void Prim(wGraph* g,wGraph* t,char Start)
{
//将t初始化
*t = initWgraph(g->vertexNumber);
bool *V = (bool*)malloc(sizeof(bool) * g->vertexNumber);
//V集合初始值為true表示這個集合裡的元素都可用,false表示集合中元素被删掉了
for(int i = 0; i < g->vertexNumber;++i)
V[i] = true;
int cnt = g->vertexNumber;
bool *U = (bool*)malloc(sizeof(bool) * g->vertexNumber);
//U集合初始值為false,表明這個集合中還沒有任何元素,true表示這個元素可用
for(int i = 0; i < g->vertexNumber;++i)
U[i] = false;
//每次從V中選中一個元素,則把它的V[i]設為false,把U[i]設為true
int start = Start-'A';
V[start] = false;
--cnt;
U[start] = true;
while(cnt >0)
{
int indexV;
int indexU;
//在U與V-U中尋找權值最小的邊
selectMiniWeightArc(g,U,V,&indexV,&indexU);
char vertex1 = indexV+'A';
char vertex2 = indexU+'A';
// printf("挑出V中的節點%c,U中的節點%c\n",vertex1,vertex2);
addArc(t,vertex1,vertex2,g->matrix[indexV][indexU]);
V[indexU] = false;
U[indexU] = true;
--cnt;
}
free(U);
free(V);
}
void selectMiniWeightArc(wGraph* g,bool* u,bool* v,int* indexV,int* indexU)
{
int min = INT_MAX;
//列印一下兩個數組的現狀
printf("V:");
for(int i = 0; i < g->vertexNumber;++i)
printf("V[%d] = %d ",i,v[i]);
printf("\n");
printf("U:");
for(int i = 0; i < g->vertexNumber;++i)
printf("U[%d] = %d ",i,u[i]);
printf("\n");
//周遊整個圖
for(int i = 0; i < g->vertexNumber;++i)
{
for(int j = 0; j < g->vertexNumber;++j)
{
//i是V中的,j是U中的
if(false == v[i] && true == u[i] && true == v[j] && false == u[j])
{
if(g->matrix[i][j] < min)
{
min = g->matrix[i][j];
*indexV = i;
*indexU = j;
}
}
}
}
}
程式中需要注意,當兩個頂點不連通是,我對他們之間的權值設定為INT_MAX,但是在列印時,為了好看一點,顯示NULL。具體的算法程式是嚴格按照算法的步驟寫的,基本沒有什麼花架子。唯一需要說明的是,在Kruskal算法中,需要檢測是否形成回路回路,這個問題可讓我費了一番周折。因為沒有學過離散數學,是以也沒有用什麼好的辦法,而是用了一個比較樸素的辦法:深度優先周遊整個圖:從A找到C以後,把A->C與C->A的兩條路都标記了起來。然後繼續向下查找,如果最後能找回來,則說明有回路。
具體的查找過程可以通過注釋起來的列印資訊看出來,這裡就不羅嗦了。
最後簡單的比較這兩種算法:Prim算法使用的資料結構稍微有點複雜,用兩個數組記錄了頂點是否被使用過,但是它很好地避免了回路檢測這個問題;而Kruskal算法則上手比較容易,更符合我們的基本思維,但是回路檢測過程卻效率低下(這也與我的回路檢測算法有關)。總體上來說,Prim算法跟勝一籌。