6.1 什麼是圖
圖是描述多對多關係的結構,圖包含頂點和邊,頂點用V(Vertex)表示,邊用E(Edge)表示,雙向邊用(v,w)圓括號括住的頂點對錶示,單向邊用<v,w>表示。
操作集:
Graph Create();
Graph InsertVertex(Graph G, Vertex V);
Graph InsertEdge(Graph G, Edge E);
void DFS(Graph G, Vertex V); //從V出發深度優先遍歷圖G
void BFS(Graph G, Vertex V); //從V出發廣度優先遍歷圖G
void ShortestPath(Graph G, Vertex V, int Dist[]);//計算圖G中頂點V到其他任意頂點的最短距離。
void MST(Graph G);//計算圖G的最小生成樹
怎麼在程序中表示圖:
鄰接矩陣:
對於一個圖有N個節點,節點從0到N-1進行編號,用一個二維數組存儲G[i,j],如果第i個元素和第j個元素之間有邊,那麼就把G[i,j]的值設置爲1,否則設置爲零。
對於無向圖(即兩個節點之間的邊沒有方向,G[i,j]和G[j,i]等效),我們可以省略一半的存儲空間,用一個(N*(N-1)/2)大小的一維數組來存儲,要找i,j之間的邊可以用(i*(i+1)/2+j)來索引。有向圖不可省略。
鄰接矩陣查找某個節點鄰接的節點很方便,只需掃描第i行的元素是否爲1即可,對於有權圖,把數組值改爲權值即可,對於有向圖,ij表示從i到j,ji表示從j到i。
鄰接矩陣存儲稀疏圖(節點很多邊很少)會非常浪費空間,比較適合存稠密圖。
鄰接表:
稀疏圖用鄰接矩陣會浪費很多空間,那麼我們可以用鄰接表來存儲稀疏圖,鄰接表是一個鏈表類型的數組,有N個節點就有N個元素,每個元素的值都是一個鏈表頭,這個鏈表鏈接着這個元素對應節點的所有的邊,無所謂順序 ,一個接一個把邊存下來,鄰接表存稀疏矩陣比較好,但是終究不如鄰接矩陣方便。
6.2 圖的遍歷
圖的遍歷有兩種方式:DFS(深度優先搜索)和BFS(廣度優先搜索)。
- DFS:就是從某一個節點開始,依次訪問它的鄰接點,每個鄰接點都遞歸的調用DFS方法。
void DFS(Vertex V){
V.Visited = true;
//對於鄰接表來說,訪問V的每一個鄰接點就是找到V對應的鏈表依次訪問
//對於鄰接矩陣來說,需要訪問V對應的那一行裏所有的非零(或無窮)項
for(V的每一個鄰接點W){
if(W沒有被訪問過){
DFS(W);
}
}
}
- BFS:廣度優先搜索,類似於樹的層序遍歷,用隊列實現。
void BFS(Vertex V){
V.Visited = true;
Queue Q;
AddQ(Q,V);
while(!IsEmpty(Q)){
V = Delete(Q);
for(V的每一個鄰接點W){
if(W沒有被訪問){
V.Visited = true;
AddQ(Q,W);
}
}
}
}
兩種遍歷方法各有優劣,不同的情況適用不同的方法。
有的時候圖並不是聯通的,這時候要遍歷,需要把圖中的每個沒有被訪問的節點都調用一次BFS或DFS,就可以把每個節點都訪問到。(兩種遍歷方法是檢索數據的方式,並不是說找不到這些數據了,而是按照某種特定方法來遍歷,達到某些目的)。
7.1 最短路徑問題
最短路徑問題分爲:單源最短路徑和多源最短路徑。
單源最短路徑:
單源無權圖的算法思想:從源點開始一圈一圈往外擴展,依次找到與源點距離爲1的,與源點距離爲2的節點,對廣度優先搜索(BFS)稍作修改即可。
void Unweighted( Vertex S ){
int Dest[N] = {-1};
Vertex Path[];
EnQueue( S,Q );
Dest[S] = 0;
while( !IsEmpty Q ){
V = DelQueue(Q);
for(V的每一個鄰接點W){
if( Dist[W] == -1){
//到W的距離等於到V的距離加1
Dist[W] = Dist[V]+1;
//到達W的最短路徑必須經過V
Path[W] = V;
}
}
}
}
- 單源有權圖的算法(dijkstra算法):有一個集合S,它裏面收錄了源點和已經找到最短路徑的點,按照距離非遞減的順序依次把所有的點都收錄到S裏面。
//需要把Dist初始化爲正無窮,Path初始化爲-1
void Dijkstra(VerTex V){
Dest[V] = 0;
Collected[V] = true;
for(V的每一個鄰接點W){
//E<V,W>bi表示V到W的距離,也就是權重。
Dest[W] = E<V,W>;
}
while(1){
V = 還未收錄的節點的Dest最小的節點。
if(所有的節點都被收錄)break;
Collected[V] = true;
for(V的每一個鄰接點){
if( Collected[W] == false ){
if(Dist[V] + E<V,W> < Dist[W]){
Dist[W] = Dist[V] + E<V,W>;
Path[W] = V;
}
}
}
}
}
多源最短路徑:
方法一:可以直接把單源最短路徑的方法對每一個節點都調用一遍(對稀疏圖效果好)。
方法二:Floyd算法
bool Floyd( MGraph Graph, WeightType D[][MaxVertexNum], Vertex path[][MaxVertexNum] )
{
Vertex i, j, k;
/* 初始化 */
for ( i=0; i<Graph->Nv; i++ )
for( j=0; j<Graph->Nv; j++ ) {
D[i][j] = Graph->G[i][j];
path[i][j] = -1;
}
for( k=0; k<Graph->Nv; k++ )
for( i=0; i<Graph->Nv; i++ )
for( j=0; j<Graph->Nv; j++ )
if( D[i][k] + D[k][j] < D[i][j] ) {
D[i][j] = D[i][k] + D[k][j];
if ( i==j && D[i][j]<0 ) /* 若發現負值圈 */
return false; /* 不能正確解決,返回錯誤標記 */
path[i][j] = k;
}
return true; /* 算法執行完畢,返回正確標記 */
}
8.1 最小生成樹問題
最小生成樹: 一個圖構成最小生成樹,圖裏面的每一個聯通的節點都必須包含在生成樹裏面,生成樹的邊最少即(N-1)條,不構成迴路,而且選出的邊的權重和必須最小。
有兩種算法:Prim算法和KrusKal算法
- Prim算法:從一個根節點開始,依次選出可以選的邊,選擇的邊要是權重最小的,且不會構成迴路。適用於邊比節點多很多。
void prim(VerTex V){
//根節點的parent就是-1,非根節點的parent是它的父元素。
parent[V] = -1;
//根節點的dest爲0,根節點的鄰接點dest爲邊的距離,其餘的dest都是無窮大。
dest[V] = 0;
for(V的每一個鄰接點W){
dest[W] = E<V,W>;
}
while(1){
V = 全部節點中dest最小的非零節點;
if(沒有這樣的節點V)
break;
//將V收錄進MST
dest[V] = 0;
for(V的每一個鄰接點W){
//如果W沒有被收錄,即W的dest不爲零
if(dest[W] != 0 && E<W,V> < dest[W]){
dest[W] = E<W,V>;
parent[W] = V;
}
}
}
if(MST中的節點不足|V|個)
Error("生成樹不存在!")
}
- KrusKal算法:每次選擇權重最小的,不會構成迴路的邊。適用於邊和節點屬於同一數量級的
void kruskal(VerTex V){
//MST初始爲空。
MST = {};
while(MST中的邊不到N-1條 && 邊集E中還有邊存在){
//最小堆
//個人思路:構造一個存儲邊的結構體,包含這條邊兩端的節點信息
從E中取出一條權重最小的邊E<V,W>;
將E<V,W>從邊集中刪除;
//並查集檢查選中邊的兩個節點是否在同一個集合中
//個人思路:將邊加入MST中時,同時將邊的兩個節點標記爲已經加入,判斷是否構成迴路就判斷這條邊兩個節點是否都已經加入。
if(E<V,W>添加到MST中不會構成迴路)
將E<V,W>加入MST中
}
if(MST中的邊不到N-1條)
Error("生成樹不存在");
}
8.1 拓撲排序
拓撲序: 如果圖中從V到W有一條有向路徑,則V一定排在W之前。滿足此條件的頂點序列稱爲一個拓撲序。獲得一個拓撲序的過程就是拓撲排序。AOV如果有合理的拓撲序,則必定是一個有向無環圖(DAG)。
void topSort(){
for(圖中的每一個頂點V)
if(Indegree[V] == 0)
InQueue(Q,V)
while(!IsEmpty(Q)){
V = Dequeue(Q);
輸出V,或者記錄V的輸出序號。
for(V的每一個鄰接點W)
if(--Indegree[W] == 0)
InQueue(Q,W)
}
if(輸出的個數不足|V|個)
Error("圖中有迴路");
}