主要內容
現實生活中的許多問題都可以轉化爲圖來解決。例如,如何以最小成本構建一個通信網絡,如何計算地圖中兩地之間的最短路徑,如何爲複雜活動中各子任務的完成尋找一個較優的順序等。
下面將介紹圖的四個常用算法:最小生成樹、最短路徑、拓撲排序和關鍵路徑。
最小生成樹
假設要在n個城市之間建立通信聯絡網,則連通n個城市只需要n-1條線路。這時,自然會考慮如何在最省經費的前提下完成任務。
在一個連通網的所有生成樹中,各邊的代價之和最小的那棵生成樹稱爲該連通網的最小生成樹。
MST性質:最小生成樹中必定存在一條具有最小權值的邊。普利姆(Prim)算法和克魯斯卡爾(Kruskal)算法是兩個利用MST性質構成最小生成樹的算法。
普利姆算法的核心思想是歸併點,時間複雜度爲O(n²),適用於稠密網;
克魯斯卡爾算法的核心思想是歸併邊,時間複雜度爲O(elog2e),使用與稀疏網。
普利姆算法(加點法)
<邏輯思路>
(1)設所有頂點保存在集合V中,已被歸併的點保存在集合U中,則未被歸併的點保存在集合V-U中;
(2)在圖中任意找一個起始頂點v1,v1歸入U,離開V-U;
(3)頂點v1存在v2,v3,v4三個鄰接頂點,找出權值最小的邊(v1,v3);
(4)頂點v3歸入U,離開V-U;
(5)頂點v1剩餘鄰接頂點v2,v4,頂點v3有鄰接頂點v2,v4,v5,v6;
(6)比較(v1,v2)和(v3,v2),得出權值更小邊(v3,v2);比較(v1,v4)和(v3,v4),兩邊權值相同;
*在邏輯思路中,其實第(6)步可以省略,直接比較所有邊的權值,再從中選擇權值最小的邊。但代碼實現中,應該避免數據冗餘,先篩選部分意義重合的數據。
(7)比較(v3,v2),(v1,v4)或(v3,v4),(v3,v5),(v3,v6),找出權值最小的邊(v3,v6);
(8)頂點v6歸入U,離開V-U;
(9)頂點v1剩餘鄰接頂點v4(因爲(v3,v2)的權值更小,所以不再需要考慮v1到v2的情況),頂點v3剩餘鄰接頂點v2,v4,v5,頂點v6有鄰接頂點v4,v5;
(10)到這裏思路應該清晰了。
<實現思路>——以鄰接矩陣爲存儲結構的無向網
(1)頂點集合爲V等價於鄰接矩陣圖中用於存儲頂點信息的一維數組vexs[vexnum];
(2)算法最巧妙的地方——結構體數組closedge[vexnum],包含信息:最小邊在集合U中的那個頂點(adjvex)和最小邊的權值(lowcost)。
結構體數組closedge[]的使用正是<邏輯思路>中步驟(6)的體現。
closedge[vi-1]表示頂點vi,當lowcast不爲0時,vi在集合V-U中;當lowcast記爲0時,vi歸併到集合U中;
(3)循環執行某一段代碼,直至closedge[]中所有元素的lowcast屬性都歸0,即所有頂點都併入到集合U中。
(看代碼前可以先回顧下“鄰接矩陣”的知識)
typedef struct /*定義結構體數組closedge[vexnum]*/
{
Vextype adjvex;
Arctype lowcast;
} closedge[vexnum];
void MiniSpanTree_Prim(AMGraph G, Vextype vi)
{
int i = LocateVex(G, vi); /*確定起始頂點vi的編號*/
closedge[i] = {NULL, 0}; /*將vi歸併到集合U中*/
for(int vj = 1; vj <= G.vexnum; vj++) /*對於V-U中的每個頂點vj,初始化closedge[vj-1]*/
{
int j = LocateVex(G, vj);
if(j != i) closedge[j] = {vi, G.arcs[i][j]};
}
for(int k = 1; k < G.vexnum; k ++) /*直到所有頂點歸併到集合U前,循環執行某一段代碼*/
{
i = Min(closedge); /*函數Min()找出closedge[]中lowcast最小的元素,並返回下標i*/
/*即找出權值最小的邊,並找出位於V-U中的頂點vj*/
closedge[i].lowcast = 0; /*將頂點vj歸併到集合U中*/
u0 = closedge[i].adjvex /*u0爲最小邊在U中的點*/
v0 = G.vexs[i]; /*v0爲最小邊在V-U中的點*/
cout<<u0<<v0; /*輸出u0,v0以記錄路線*/
for(int j = 0; j < G.vexnum; j++)
{
/*之前對closedge[]的元素進行過初始化,併入新的點後要重新選出權值更小的邊,對應<邏輯思路>的步驟(6)*/
if(G.arcs[i][j] < closedge[j].lowcast)
closedge[j] = {G.vexs[i], G.arcs[i][j]};
}
}
克魯斯卡爾算法(加邊法)
如果說普利姆算法是“加點法”,那麼克魯斯卡爾算法就是“加邊法”。
<邏輯思路>
(1)將由n個頂點組成的連通圖拆分成n個連通分量,即每個頂點爲一個連通分量;
(2)將圖上的所有邊按權值排序;
(3)從最小邊開始操作,歸併邊的判斷條件是下一條被選邊不能使連通分量形成迴路,即被選邊的兩個頂點head和tail不能在同一個連通分量上;
(4)在循環中執行某段代碼,直至所有頂點被歸併到同一連通分量。
<實現思路>——以鄰接矩陣爲存儲結構的無向網
(1)按權值排序可以使用“冒泡法”和“選擇法”;
(2)從步驟(3)可以看出,我們需要類似於普利姆算法中的closedge[]那樣的輔助數組。包含的信息:每一條邊的頭頂點和尾頂點以及邊上的權值;
(3)同時,我們還需要一個標記數組輔助判斷每一個頂點所屬的連通分量;
(4)歸併一個頂點後,將頂點的連通分量改爲併入它的連通分量。
typedef struct /*結構體數組的各元素代表各邊*/
{
Vextype head;
Vextype tail;
Arctype lowcast;
} Arcs[arcnum];
int VexSet[vexnum]; /*VexSet[vi-1] = i;表示vi所在的連通分量編號爲i,即它本身*/
void MiniSpanTree_Kruskal(AMGraph G)
{
for(int t = 0; t < arcnum; t++) /*輸入各邊信息*/
cin>>Arcs[t].head>>Arcs[t].tail>>Arcs[t].lowcast;
Sort(Arcs); /*按權值將圖上各邊從小到大排序*/
for(int t = 0; t < arcnum; t++) /*對圖上所有邊(權值從小到大)依次進行操作*/
{
Headv = LocateVex(G, Arcs[t].head); /*確定頭尾頂點的編號*/
Tailv = LocateVex(G, Arcs[t].tail);
VS_h = VexSet[HeadV]; /*確定頭尾頂點所在的連通分量*/
VS_t = VexSet[Tailv];
if(VS_h != VS_t) /*若兩個頂點不在同一連通分量*/
{
cout<<Arcs[t].head<<Arcs[t].tail; /*輸出符合要求的邊*/
for(int v = 0; v < G.vexnum; v++) /*對所有頂點進行判斷*/
if(VexSet[v] == VS_t)
VexSet[v] = VS_H; /*將頂點歸併到同一連通分量*/
}
}
}
最短路徑
最簡單的最短路徑是求中轉次數最少的路徑,而不考慮每條邊的權值。而在實際問題中,路徑長度的度量就不再是路徑上的邊數,而是路徑上所有邊的權值之和。
在有向網中,習慣上稱路徑的第一個頂點爲源點(Source),最後一個頂點爲終點(Destination)。
下面主要討論兩種最常見的最短路徑問題:
一、從某個源點到其餘頂點的最短路徑;
二、求每一對頂點之間的最短路徑。
迪傑斯特拉(Dijkstra)算法用於求解第一種問題,時間複雜度爲O(n²);弗洛伊德(Floyd)算法用於求解第二種問題,時間複雜度爲O(n³)。
從實現形式上來說,弗洛伊德算法比迪傑斯特拉算法更爲簡潔。
迪傑斯特拉算法(單源點)
<邏輯思路>
(1)從單源點出發,求到各個頂點的最短路徑。該問題類似於就有向連通網的最小生成樹;
(2)因此同樣需要輔助數組來記錄頂點是否被歸併和最短路徑長度;
(3)迪傑斯特拉算法的巧妙之處就在於設計了三個輔助數組:
1. 一維數組S[]:記錄頂點vi是否被確定最短路徑長度,即該頂點是否被歸併到終點集合中。初始化:S[v0-1] = TRUE;
2. 一維數組Path[]:記錄頂點vi的直接前驅。譬如存在最短路徑<v1,v4,v3>,v4的直接前驅是v1,v3的直接前驅是v4,這樣一來,依靠直接前驅數組就能將路徑連接起來。初始化:若源點v0到vi有弧,則Path[vi-1]爲v0-1(編號/下標);否則爲-1。
3. 一維數組D[]:記錄從源點v0到終點vi的最短路徑長度。初始化:若源點v0到vi有弧,則D[vi-1]爲弧上的權值;否則爲∞。
(4)算法開始時,先找到D[]上的最小值,然後併入第一個終點v1,並將S[v1-1]的值設爲TRUE;
(5)由於加入了終點v1,相當於v0多了一個中轉點,所以需要對D[]上的值進行更新;
(6)更新後繼續找D[]上的最小值,繼而找到了頂點v2,因爲S[v2-1]的值爲FALSE,即v2未被歸併到終點集合,所以可以將它併入;
(7)若到v2的最短路徑是<v0,v1,v2>,則將v2的直接前驅改爲v1,即Path[v2-1] = v1-1;
(8)反覆執行以上過程(n-1次),直至所有頂點被併入終點集合。
<實現思路>——以鄰接矩陣爲存儲結構的有向網
(1)對應<邏輯思路>步驟(3)的初始化:
1. S[v0-1] = TRUE;
2. D[vi-1] = G.arcs[v0-1][vi-1];
3. if(G.arcs[v0-1][vi-1] != MaxInt) Path[vi-1] = v0-1; /*MaxInt表示無窮∞,值爲326767*/
(2)對應<邏輯思路>步驟(8)的n-1次循環
1. 步驟(4):D[v1] = Min(D);
2. 步驟(6):if(S[v1-1] = FALSE) S[v1-1] = TRUE;
3. 步驟(5)和步驟(7): if(D[v1-1] + G.arcs[v1-1][v2-1] < D[v2-1]) {D[v2-1] = D[v1-1] + G.arcs[v1-1][v2-1]; Path[v2-1] = v1-1;}
#define TRUE 1
#define FALSE 0
void ShortestPath_DIJ(AMGraph G, int v0)
{
int S[vexnum];
int Path[vexnum];
int D[vexnum];
int i = LocateVex(G, v0);
for(int vi = 1; vi <= G.vexnum; vi++) /*初始化*/
{
int j =LocateVex(G, vi);
S[j] = FALSE;
D[j] = G.arcs[i][j];
if(D[j] < MaxInt) Path[j] = i; /*小於MaxInt即v0與vi間存在弧*/
else Path[j] = -1; /*若vi的直接前驅不是v0,則置爲-1*/
}
S[i] = TRUE;
D[i] = 0; /*源點到源點的路徑長度爲0*/
/*----------初始化結束-----------*/
for(int t = 1; t < G.vexnum; t++) /*循環n-1次*/
{
int v; /*下一個終點*/
for(vi = 1; vi <= G.vexnum; vi++)
{
int min = MaxInt; /*min保存最小值*/
int j = LocateVex(G, vi);
if(S[j] = FALSE && D[j] < min)/*找出最小權值的邊*/
{v = vi; min = D[j];}
}
S[v-1] = TRUE; /*將v加入終點集合*/
int k = LocateVex(G, v);
for(vi = 1; vi <= G.vexnum; vi++) /*更新最短路徑和直接前驅數組*/
{
int j = LocateVex(G, vi);
if(S[j] = FALSE && (D[k] + G.arcs[k][j] < D[j])
{
D[j] = D[k] + G.arcs[k][j];
Path[j] = k;
}
}
}
}
弗洛伊德算法(頂點對)
求解每一對頂點之間的最短路徑有兩種方法:
一種是n次調用迪傑斯特拉算法;
另一種是採用下面介紹的弗洛伊德算法。
兩種算法的時間複雜度均爲O(n³),但弗洛伊德算法的形式更爲簡潔。
<邏輯思路>
(1)如果說迪傑斯特拉算法是一個“加邊”的過程,那麼弗洛伊德算法就是一個“加點”的過程;
(2)先確定兩個目標頂點,源點爲vs,終點爲ve;
(3)若vs和ve之間存在弧,初始化爲arcs[vs-1][ve-1](編號),反之置爲∞;
(4)依次判斷剩餘頂點vi,若在兩個頂點間插入vi後,使得<vs,vi>+<vi,ve>小於<vs,ve>,則置換爲vs和ve間的最短路徑;
(5)直至所有剩餘頂點判斷完畢,最短路徑確定;
(6)其餘的每一對頂點都重複上述過程。
<實現思路>——以鄰接矩陣爲存儲結構的有向網
(1)該算法的核心思想是設置了兩個二維輔助數組:
1. 二維數組Path[][]:行表示頂點vi的直接前驅,列表示頂點vi;
2. 二維數組D[]][]:記錄vs和ve間的最短路徑。
(2)弗洛伊德算法的代碼思路比較清晰,插入、比較、更新。
void ShortestPath_Floyd(AMGraph G)
{
/*-----------初始化------------*/
for(int vs = 1; vs <= G.vexnum; vs++) /*源點vs*/
{
int i = LocateVex(G, vs);
for(int ve = 1; ve <= G.vexnum; ve++) /*終點ve*/
{
int j = LocateVex(G, ve);
D[i][j] = G.arcs[i][j];
if(D[i][j] < MaxInt && i != j)
Path[i][j] = i;
else Path[i][j] = -1;
}
/*---------初始化結束--------*/
for(int vi = 1; vi <= G.vexnum; vi++) /*插入點vi*/
for(int vs = 1; vs <= G.vexnum; vs++) /*源點vs*/
for(int ve = 1; ve <= G.vexnum; ve++) /*終點ve*/
{
int k = LocateVex(G, vi);
int i = LocateVex(G, vs);
int j = LocateVex(G, ve);
if(D[i][k] + D[k][j] < D[i][j]) /*更新*/
{
D[i][j] = D[i][k] + D[k][j];
Path[i][j] = k;
}
}
}
路過的圈毛君:“‘拓撲排序’和‘關鍵路徑’的內容可能要先放一放,以後一定會補上的!_(:з」∠)_”