關於圖的演算法,記錄對於圖的理解
阿新 • • 發佈:2021-01-26
技術標籤:資料結構和演算法分析演算法資料結構圖論圖解法抽象類
關於圖的演算法
文章目錄
- Dargon
- 2021/01/24
- 所遇到的的重要的問題: 加深自己能理解 或者記住自己現階段對於
圖
的演算法理解 - 教科書 來自:《大話資料結構》第七章 圖
01 深度優先遍歷(Deepth_First_Search)
- 事實上 深度優先遍歷就是一個,深度優先函式進行遞迴的過程,他從圖中某個頂點Vertex出發,訪問此頂點,然後從v的未被訪問的鄰接點出發進行 開始深度優先遍歷圖,直到圖中所有的和V有路徑相通的頂點都被訪問到。
- 主要感覺 當遞迴返回上一層這種感覺時候,可以處理掉上一層的點,就能解決一些很大問題。
1.1 鄰接矩陣的實現
- 關於程式碼:
/* 深度優先遞迴 */
void DFS_matrix( GraphMatrix G, int i ) {
int j;
visited[i] =TRUE;
printf("%c", G.vexs[i]);
for( j =0; j <G.numVertexes; j++ ) {
// 遞迴思想真舒服!一層一層去看待問題 想一想走出迷宮點燈的問題!!
if( G.arc[i][j] ==TRUE && !visited[j] ) {
DFS_matrix( G, j ) ;
}
}
}
/* 深度遍歷演算法 */
void DFS_matrix_traverse( GraphMatrix G ) {
int i;
/* 初始化numVertexes 個頂點狀態 visited[] 矩陣 */
for( i =0; i <G.numVertexes; i++ ) {
visited[i] =FALSE;
}
// 對 未訪問 過的頂點呼叫 DFS,若是連通圖 只會執行一次
for( i =0; i <G.numVertexes; i++ ) {
if( !visited[i] ) {
DFS_matrix( G, i );
}
}
}
- 對於一個圖,找一個頂點,按照一個規定方向,遞迴開始,比如在迷宮問題中 從右邊開始走,到一個頂點,當作起始點,再次遞迴進行(對於其他的,沒有檢視到的頂點,在遞迴返回到上一層後 會順帶解決它們)。
- 所以對於一個圖來說,如果頂點都是聯通的話,從一個頂點開始,直接通過遞迴就可以將所有的頂點都能訪問到。
1.2 鄰接表的實現
- 關於程式碼:
- 理解:
對於訪問的頂點,利用遞迴呼叫的時候,使用連結串列指標代替for迴圈。
02 廣度優先遍歷(Breadth_First_Search)
- 圖的深度優先搜尋類似樹的前序遍歷, 則廣度優先遍歷類似於 層序遍歷,大概理解 我走的不深 不是一頭紮下去走到最後,而是一層一層的去訪問(Vertex)節點。
2.1 鄰接矩陣的實現
- 關於程式碼:
void BFS_matrix_traverse( GraphMatrix G ) {
int i, j;
QueueNode Q;
/* 進行初始化 */
for( i =0; i <G.numVertexes; i++ ) {
visited[i] =FALSE;
}
queue_init( &Q );
/* 對於每一個頂點做迴圈處理 */
for( i =0; i <G.numVertexes; i++ ) {
/* 若是沒有訪問 就對該頂點進行處理 */
if( !visited[i] ) {
visited[i] =TRUE;
printf("%c", G.vexs[i]);
queue_add( &Q, i ); /* 將此頂點入列 */
while( !queue_empty( Q ) ) {
queue_delete( &Q, &i ); /* 將隊中元素 出列 把值賦予 i */
for( j =0; j <G.numVertexes; j ++ ) {
if( G.arc[i][j] ==1 && !visited[j] ) {
visited[j] =TRUE; /* 把與第 i 行鄰接的 都去訪問 層序遍歷 */
printf("%c", G.vexs[j]);
queue_add( &Q, j );
}
}
}
}
}
}
- 從一個頂點(Vertex)開始,將此頂點入佇列,在while()迴圈中,再將頂點彈出佇列,保留下標值,然後找與此頂點所連結的頂點 分別操作 標記為已訪問,然後進入佇列。
- 再次進入while() 迴圈,將第一個連線點 彈出佇列,標記訪問其連線點 並進行入隊。
- 這就基本形成 一層一層 進行訪問的效果, 層序遍歷。
2.2 鄰接表的實現
- 關於程式碼:
- 理解:
將while()裡面的迴圈,將與頂點(Vertex)的連線點,變成指標連結串列去尋找下一個節點,不是依靠矩陣去搜尋。
03 最小生成樹(Minimum Cost Spanning Tree)
- 將一顆具有(n)個頂點Vertex的圖,生成一個具有(n-1)條邊的樹,且在邊中具有權值的時候,將所有的權值綜合儘量的小 就是所謂的最小生成樹。
3.1 普利姆演算法(Prim Algorithm)
- 關於程式碼:
void mini_span_tree_prim(GraphMatrix G) {
int min, i, j, k;
int adjvex[MAXVEX]; /* 儲存相關頂點下標 */
int lowcost[MAXVEX]; /* 儲存相關頂點間的權值 */
lowcost[0] =0; /* 初始化第一個權值 為0 */
adjvex[0] =0; /* 初始化第一個頂點下標為0 */
/* 初始化 */
for( i =1; i <G.numVertexes; i++ ) {
lowcost[i] =G.arc[0][i]; /* 將V0頂點與之有邊的權值存入陣列 相當於初始化 讀入V0 行 */
adjvex[i] =0; /* 初始化都為 V0 的下標 */
}
/* 開始執行 找最小的代價問題 */
for( i =1; i <G.numVertexes; i++ ) {
min =INFI;
j =1;
k =0;
/* 尋找lowcost[] 裡面最小值 對應的位置 就是下標 */
while( j <G.numVertexes ) {
if( lowcost[j] !=0 && lowcost[j] <min ) {
min =lowcost[j];
k =j;
}
j ++;
}
/* 找到位置 進行連線 當前頂點adjvex[k] 找到與之所相連的 最小權值 k邊 */
printf("(%d,%d)", adjvex[k], k); /* 列印所頂點 鄰接邊中權值最小的邊K */
lowcost[k] =0; /* 更新節點邊的權值 陣列 當前節點完成任務 相應的k 邊為0 */
/* 找與k 邊所連線的邊的值 更新lowcost 同事更新adjvex[] 以便下一次找出這條邊的最小值 */
for( j =1; j <G.numVertexes; j++ ) {
if( lowcost[j] !=0 && G.arc[k][j] <lowcost[j] ) {
lowcost[j] =G.arc[k][j];
adjvex[j] =k; /* 此時 將一輪更新中的對應於上一輪的最小值 下標k 存入adjvex[]中 */
/* 存下標 主要是方便 輸出鄰邊 */
}
}
}
}
- 在初始化中,宣告兩個陣列lowcost[] 和adjvex[], 陣列lowcost[] 初始化為與V0 鄰接的矩陣的值,事後將後面的小值更新到此數組裡面進來。adjvex[] 陣列初始化用來記錄下標。
- 在lowcost[] 數組裡面找到最小值,記錄下標,然後將此下標對應的lowcost[] 裡的值變成 0(沒有特殊意義 表示此節點已經是最小值 不用更改了),在利用與此下標所連線的邊的權值,與lowcost[] 數組裡面對應的值進行比較,用兩者較小的值去更新陣列。
- 再次迴圈找 數組裡面 最小的值,記錄下標,將對應下標的lowcost[]元素 進行清零,更新陣列。
- 最終會將lowcost[] 裡面基本都變成小值,演算法執行的順序就是按照 在lowcost[] 裡面的非0 的最小值,去進行一次次的迴圈 ,並用新的 較小值更新陣列。
- Adjvex[] 元素的每一步的生成順序,記錄著最小樹的生成過程。
3.2 克魯斯卡爾演算法(Kruskal Algorithm)
- Prime演算法是從頂點的角度考慮,而Kruskal演算法則從 邊的角度去考慮
- 程式碼如下:
int kruskal_find( int *parent, int f ) {
while( parent[f] >0 ) {
f =parent[f];
}
return f;
}
void mini_span_tree_kruskal( GraphMatrix G ) {
int i, j, n, m;
int flag =0;
EdgeList edges[MAXVEX];
EdgeList temp;
int parent[MAXVEX];
/* 矩陣轉化 觀察如何加進去 */
/* 將矩陣讀取到edges 裡面 */
for( i =0; i <G.numVertexes-1; i++ ) {
for( j =i +1; j <G.numVertexes; j++ ) {
if( G.arc[i][j] <INFI ) {
edges[i].begin =i;
edges[i].end =j;
edges[i].weight =G.arc[i][j];
}
}
}
/* 進行 Bubble 排序 */
for( i =G.numEdges -1; i >=0; i-- ) {
flag =0;
for(j =0; j <i; j++) {
if( edges[j].weight >edges[j +1].weight ) {
temp =edges[j];
edges[j] =edges[j +1];
edges[j +1] =temp;
flag =1;
}
}
if( flag ==0 ) break;
}
/* 陣列 parent 的初始化 */
for( i =0; i <G.numVertexes; i++ ) {
parent[i] =0;
}
for( i =0; i <G.numEdges; i++ ) {
n =kruskal_find( parent, edges[i].begin );
m =kruskal_find( parent, edges[i].end );
if( n !=m ) { /* 判斷此時 樹中是否 有迴路生成 */
parent[n] =m; /* 若有迴路生成 則有n =m出現 目的是找到最小的 且不出現環的現象 */
printf("(%d, %d) %d", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
- 將鄰接矩陣轉化為鄰接表的形式,並且按照權重值進行從小到大的順序進行排列。
- 整體是按照順序去連線,注意 此時不能形成環,即是(n != m),對於此處的判斷,應該是 對於已連線的點,從begin 找到 end檢視值 若相同 就是已經形成環了 就是封閉了,若沒有 則加入生成樹中。
- 整體來說 邊數少的時候 很有效。
04 最短路徑
- 從源點到目標點的所經過的邊的權值和 是最小的一條路 和最小生成樹還是有些區別的
4.1 迪傑斯特拉演算法(Dijkstra Algorithm)
- 關於程式碼:
/* 演算法對應的是隻找出V0 頂點 到各頂點所對應的 最短路徑 其實 若是需要找每一個頂點 也同樣需要 O(N^3)複雜度 */
void shortest_path_dijkstra( GraphMatrix G, int v0, Patharc *P, ShortPathTable *D ) {
int v, w, k, min;
int final[MAXVEX]; /* 表示最短路徑 */
/* 相當於各陣列的資料進行初始化 */
for( v =0; v <G.numVertexes; v++ ) {
final[v] =0;
(*D)[v] =G.arc[v0][v]; /* 將和v0有關的連線加上權值 */
(*P)[v] =0;
}
(*D)[v0] =0;
final[v0] =1;
/* 開始主迴圈 每次求v0 到某個v 頂點的最短路徑 */
for( v =1; v <G.numVertexes; v++ ) {
min =INFI;
for( w =0; w <G.numVertexes; w++ ) {
/* 在D 中尋找離 v0最近的點 */
if( !final[w] && (*D)[w] <min ) {
k =w;
min =(*D)[w];
}
}
/* 此迴圈結束 將找到的點進行處理 */
final[k] =1; /* 將目前找到的最近的頂點 下標 為1 */
/* 在新的頂點上 更正D陣列的 為尋找距離v0點的最小值 */
for( w =0; w <G.numVertexes; w++ ) {
/* 若果經過v 頂點的路 比現在D 數組裡面的路徑 短的話 */
if( (!final[w]) && ( min +G.arc[k][w] <(*D)[w] ) ) {
/* 找到最短路徑 進行修改 */
(*D)[w] =min +G.arc[k][w];
(*P)[w] =k;
}
}
}
}
- p[] 陣列是儲存對應最短路徑下標 D[] 陣列用於儲存源點到各點最短路徑的權值和 final[] 初始化為0 用來表示未知的狀態。
- 將D[] 初始化成為v0 的鄰接矩陣,找到D 陣列中最小值,相當於在與v0 所連線的點中,找到距離最小的,將對應的final數組裡的元素置1,然後根據這個找到的點(相當於該點作為前驅)找與該點連線的頂點 並且根據較短距離原則來更新D陣列。
- 作為前驅點記錄下標 更新記錄在P 數組裡面
- 下面每一步 都是在前面找到最短的基礎上再來尋找最小值的。
- 最終 D[] 數組裡面的內容表示v0 點到各個頂點的最短路徑,注意 每個元素 都是路徑和,P[] 相當於每個路徑都是作為前驅節點存在的,例如P[0 0 1 4 2 4 3 6 7] P[8] =7表示頂點V8的前驅節點是V7(V8 是從 V7那裡過來的),再找P[7] =6 就是V7的前驅是V6節點,再找p[6] =3 就是V6的前驅是V3節點,逐步求之 ,可以得到整個路徑。
4.2 弗洛伊德演算法(Floyd Algorithm)
- 關於程式碼:
void shortest_path_floyd( GraphMatrix G, PathMatrix *P, ShortPath *D ) {
/* 引數陣列 利用指標 傳進函式 */
int v, w, k;
/* 進行陣列的初始化 */
for( v =0; v <G.numVertexes; ++v ) {
for( w =0; w <G.numVertexes; w++ ) {
( *D )[v][w] =G.arc[v][w];
( *P )[v][w] =w;
}
}
/* 開始主要的迴圈 進行比較 */
/* k 作為中轉點 */
for( k =0; k <G.numVertexes; k++ ) { /* 對於for 迴圈 說 ++k 的意義何在!!! */
/* v 作為起點 */
for( v =0; v <G.numVertexes; v++ ) {
/* w 作為終點 */
for( w =0; w <G.numVertexes; w++ ) {
if( ( *D )[v][w] > ( *D )[v][k] +( *D )[k][w] ) {
/* 如果更小 則進行交換 */
( *D )[v][w] =( *D )[v][k] +( *D )[k][w]; /* D 矩陣進行更新 */
( *P )[v][w] =( *P )[v][k]; /* 同時 P矩陣也進行更新 */
}
}
}
}
/* 最優路徑的列印輸出 */
for( v =0; v <G.numVertexes; v++ ) {
for( w =v +1; w <G.numVertexes; w++ ) {
printf("v%d-v%d weight: %d", v, w, ( *D )[v][w]);
k =( *P )[v][w];
printf("Path : %d", v);
while( k !=w ) {
printf("-> %d", k);
k =( *P )[k][w];
}
printf("-> %d", w);
}
printf("\n");
}
}
- 基本思想也是通過找最小路徑的問題 很巧妙通過 中間點的轉換 進行更新鄰接矩陣 例如開始的時候 是從V0–>V2,為5 的路徑,若是從V0–>V1–>V2,距離為3 則需要替代。
- 相當於將
Dijkstra Algorithm
進行升級 成從任意點到任意點的最小路徑,當然前面演算法的D陣列和 P陣列自然就變成二維陣列 進行路徑和權重的記錄。 - 通過中間點找到更短值的就進行替換。最初的 D − 1 D^{-1} D−1,經過一輪輪的跟新到 D 0 , D 1 , D 2 … … D^{0},D^{1},D^{2} …… D0,D1,D2……
05 關於AOV AOE網
- AOV (Activity On Vertex Network) 分別表示活動網 有活動的先後順序,對於邊(弧)則代表著一種制約關係,具有順序。
- AOE (Activity On Edge Network) 相當於弧長值 帶有活動進行的時間的活動網。
5.1 拓撲排序
- 關於程式碼:
int topo_logical_sort( GraphList *G ) {
EdgeNode *current;
int i, k, gettop;
int top =0; /* 對於堆疊的下標 */
int count =0; /* 統計輸出點的個數 */
int *stack; /* 定義堆疊 為堆疊分配記憶體 */
stack =(int *)malloc( sizeof(int) * G->numVertexes );
/* 找出 入度 為0 的頂點 壓入堆疊 */
for( i =0; i <G->numVertexes; i++ ) {
if( G->adjlist[i].in ==0 ) {
stack[++top] =i; /* 直接將下標 入棧 */
}
}
/* 開始主迴圈 */
while( top !=0 ) {
gettop =stack[top--];
printf("%d-> ", G->adjlist[gettop].data); /* 列印頂點 */
count ++;
/* 對此頂點 所連結的後面 進行 遍歷 刪除此頂點的聯絡 使與之相連的 in 減1 */
for( current =G->adjlist[gettop].firstedge; current; current =current->next ) {
k =current->adjvex;
if( !( -- (G->adjlist[k].in) ) ) { /* 如果被減到 0 則進行 入棧操作 */
stack[++ top] =k;
}
}
}
if( count <G->numVertexes )
return ERROR;
else
return TRUE;
}
- 先轉化到鄰接表(頂點帶有入度)的形式,進行迴圈 將所有入度為0 的頂點進行堆疊 PUSH操作,然後進行POP操作,將POP出的點 將與之所相連的頂點的入度
-1
,判斷入度是否為0,是PUSH。(這就相當於 需要把本點前面的事情都做完 之後,才能來到這個節點 進行操作),相當於將 入度為0 的。
5.2 關鍵路徑
- 應該是難理解的一個演算法了
- 拓撲排序講的是解決一個工程能否順利進行的問題,而這裡我們還需要關心 總的完成時間問題。理解為最關鍵的問題,每個把步驟最長的時間,就很緊密 中間無休息的那種,成為關鍵步驟。把具有最大長度的路徑(時間),才是我們要找的關鍵路徑。
- 理解幾個引數etv(earliest time of vertex) 事件的最早發生時間 ==>ltv;
- 還有ete(earliest time of edge) 活動最早開始時間 ==> lte
- 求etv的程式碼:
#define ERROR 0
/* 全域性變數定義區域 */
int *etv, *ltv; /* etv[]: (earliest time of vertex) */
int *stack2; /* ltv[]: (latest time of vertex) */
int top2;
int topo_logical_sort_v2( GraphList *G ) {
EdgeNode *current;
int i, k, gettop;
int top =0; /* 對於堆疊的下標 */
int count =0; /* 統計輸出點的個數 */
int *stack; /* 定義堆疊 為堆疊分配記憶體 */
stack =(int *)malloc( sizeof(int) * G->numVertexes );
/* 找出 入度 為0 的頂點 壓入堆疊 */
for( i =0; i <G->numVertexes; i++ ) {
if( G->adjlist[i].in ==0 ) {
stack[++top] =i; /* 直接將下標 入棧 */
}
}
/* 升級部分 */
top2 =0;
etv =(int *)malloc( G->numVertexes*sizeof(int) ); /* etv[]陣列申請記憶體 */
for( i =0; i <G->numVertexes; i++ ) {
etv[i] =0; /* etv[]陣列 初始化 為0 */
}
stack2 =(int *)malloc( G->numVertexes *sizeof(int) ); /* 堆疊申請記憶體 */
/* 開始主迴圈 */
while( top !=0 ) {
gettop =stack[top--];
//printf("%d-> ", G->adjlist[gettop].data); /* 列印頂點 */
count ++;
stack2[++top2] =gettop;
/* 對此頂點 所連結的後面 進行 遍歷 刪除此頂點的聯絡 使與之相連的 in 減1 */
for( current =G->adjlist[gettop].firstedge; current; current =current->next ) {
k =current->adjvex;
if( !( -- (G->adjlist[k].in) ) ) { /* 如果被減到 0 則進行 入棧操作 */
stack[++ top] =k;
}
/* 求各頂點事件的最早發生值 即是對應的最大值 放到etv[] 數組裡面 */
if( etv[gettop] +current->weight >etv[k] ) {
etv[k] =etv[gettop] +current->weight;
}
}
}
if( count <G->numVertexes )
return ERROR;
else
return TRUE;
}
- 求關鍵路徑程式碼:
void critical_path( GraphList *G ) {
EdgeNode *e;
int i, j, k, gettop;
int ete, lte; /* 分別定義最早發生時間 和最遲發生時間 */
/* 拓撲序列 填滿etv[] 陣列 和 stack2 堆疊 */
topo_logical_sort_v2( G );
/* ltv[] 陣列申請記憶體 和初始化 */
ltv =(int *)malloc( G->numVertexes *sizeof(int) );
for( i =0; i <G->numVertexes; i++ ) {
ltv[i] =etv[G->numVertexes -1]; /* 以etv[] 陣列最後一位 進行ltv[] 陣列的初始化 */
}
/* 計算ltv[] */
while( top2 !=0 ) { /* 其實恰好 將其順序 顛倒過來 對每一個 gettop 所連結後面的表 進行計算 訪問 */
gettop =stack2[top2--];
for( e =G->adjlist[gettop].firstedge; e; e->next ) {
k =e->adjvex;
if( ltv[k] -e->weight <ltv[gettop] ) { /* 計算找出相應的最小的值 */
ltv[gettop] =ltv[k] -e->weight;
}
}
}
/* 列印 關鍵 路徑 ete =lte */
for( j =0; j <G->numVertexes; j++ ) {
for( e =G->adjlist[j].firstedge; e; e=e->next ) {
k =e->adjvex;
ete =etv[j]; /* 最早發生時間 */
lte =ltv[k] -e->weight; /* 最遲發生時間 */
if( ete ==lte ) { /* 最遲時間 和最晚時間 相等 則就是 關鍵時間 */
printf("<v%d,v%d> length: %d , ", G->adjlist[j].data, G->adjlist[k].data, e->weight);
}
}
}
}
- 更新etv[] 就是找出最長(最耗時的步驟)作為最早發生時間,就是從源點到該點所走路徑達到的最大時間,就是從V0在走其它路徑到這兒的話,用的時間都是 <= 最大時間。
- 同時在更新ltv[] 時候 ,對應於另一件事情 (<=最大時間的) 最短的時間 min(用最長時間 - 邊的權重值(活動時間))
- 舉個例子: etv[1] =3 ,ltv[1] =7 ,v1 這個時間最早也只能在
第三天
開始(按照總的工程進度來說話),同時 v1也可以在第七天
開始 也可以不影響工期的完成。說明中間有四天的靈活時間
,(像你是先寫作業 還是先玩是一樣的事情),如果靈活時間
沒有 ,則此時就是關鍵的路徑了 時間緊湊的感覺。 - 活動的最早發生時間ete 就像當於 etv(事件的最早發生時間),活動的最遲發生時間lte[] 就是求最小的發生時間 相當於 最遲的發生時間(你這條路線再不去做的話,就是你耽誤工程時間)(這一段 還需要細細理解 細細去品 !!!)
總結
- 第二遍去學習這個演算法,然而第一遍當時挺明白,在第二遍的時候,又有重頭再來的感覺,邏輯重新整理,又再次進入兩天出來,所以這次要把學到的,看到的,記錄下來 記錄自己現在所看到的東西,於是用一天的時間來記錄。
- 暫且把這當做過程吧! 慢啊慢啊 這應該就是學習。