1. 程式人生 > 其它 >關於圖的演算法,記錄對於圖的理解

關於圖的演算法,記錄對於圖的理解

技術標籤:資料結構和演算法分析演算法資料結構圖論圖解法抽象類

關於圖的演算法

文章目錄

  • Dargon
  • 2021/01/24
  • 所遇到的的重要的問題: 加深自己能理解 或者記住自己現階段對於的演算法理解
  • 教科書 來自:《大話資料結構》第七章 圖

01 深度優先遍歷(Deepth_First_Search)

  1. 事實上 深度優先遍歷就是一個,深度優先函式進行遞迴的過程,他從圖中某個頂點Vertex出發,訪問此頂點,然後從v的未被訪問的鄰接點出發進行 開始深度優先遍歷圖,直到圖中所有的和V有路徑相通的頂點都被訪問到。
  2. 主要感覺 當遞迴返回上一層這種感覺時候,可以處理掉上一層的點,就能解決一些很大問題。

1.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. 所以對於一個圖來說,如果頂點都是聯通的話,從一個頂點開始,直接通過遞迴就可以將所有的頂點都能訪問到。

1.2 鄰接表的實現

  1. 關於程式碼:
  2. 理解:
    對於訪問的頂點,利用遞迴呼叫的時候,使用連結串列指標代替for迴圈。

02 廣度優先遍歷(Breadth_First_Search)

  1. 圖的深度優先搜尋類似樹的前序遍歷, 則廣度優先遍歷類似於 層序遍歷,大概理解 我走的不深 不是一頭紮下去走到最後,而是一層一層的去訪問(Vertex)節點。

2.1 鄰接矩陣的實現

  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 );
                    }
                }
            }
        }
    }
}
  1. 從一個頂點(Vertex)開始,將此頂點入佇列,在while()迴圈中,再將頂點彈出佇列,保留下標值,然後找與此頂點所連結的頂點 分別操作 標記為已訪問,然後進入佇列。
  2. 再次進入while() 迴圈,將第一個連線點 彈出佇列,標記訪問其連線點 並進行入隊。
  3. 這就基本形成 一層一層 進行訪問的效果, 層序遍歷。

2.2 鄰接表的實現

  1. 關於程式碼:
  2. 理解:
    將while()裡面的迴圈,將與頂點(Vertex)的連線點,變成指標連結串列去尋找下一個節點,不是依靠矩陣去搜尋。

03 最小生成樹(Minimum Cost Spanning Tree)

  1. 將一顆具有(n)個頂點Vertex的圖,生成一個具有(n-1)條邊的樹,且在邊中具有權值的時候,將所有的權值綜合儘量的小 就是所謂的最小生成樹。

3.1 普利姆演算法(Prim Algorithm)

  1. 關於程式碼:
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[]中  */
                /* 存下標 主要是方便 輸出鄰邊 */
            }
        }
    }
}
  1. 在初始化中,宣告兩個陣列lowcost[] 和adjvex[], 陣列lowcost[] 初始化為與V0 鄰接的矩陣的值,事後將後面的小值更新到此數組裡面進來。adjvex[] 陣列初始化用來記錄下標。
  2. 在lowcost[] 數組裡面找到最小值,記錄下標,然後將此下標對應的lowcost[] 裡的值變成 0(沒有特殊意義 表示此節點已經是最小值 不用更改了),在利用與此下標所連線的邊的權值,與lowcost[] 數組裡面對應的值進行比較,用兩者較小的值去更新陣列。
  3. 再次迴圈找 數組裡面 最小的值,記錄下標,將對應下標的lowcost[]元素 進行清零,更新陣列。
  4. 最終會將lowcost[] 裡面基本都變成小值,演算法執行的順序就是按照 在lowcost[] 裡面的非0 的最小值,去進行一次次的迴圈 ,並用新的 較小值更新陣列。
  5. Adjvex[] 元素的每一步的生成順序,記錄著最小樹的生成過程。

3.2 克魯斯卡爾演算法(Kruskal Algorithm)

  1. Prime演算法是從頂點的角度考慮,而Kruskal演算法則從 邊的角度去考慮
  2. 程式碼如下:
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);
        }
    }
}
  1. 將鄰接矩陣轉化為鄰接表的形式,並且按照權重值進行從小到大的順序進行排列。
  2. 整體是按照順序去連線,注意 此時不能形成環,即是(n != m),對於此處的判斷,應該是 對於已連線的點,從begin 找到 end檢視值 若相同 就是已經形成環了 就是封閉了,若沒有 則加入生成樹中。
  3. 整體來說 邊數少的時候 很有效。

04 最短路徑

  1. 從源點到目標點的所經過的邊的權值和 是最小的一條路 和最小生成樹還是有些區別的

4.1 迪傑斯特拉演算法(Dijkstra Algorithm)

  1. 關於程式碼:
/* 演算法對應的是隻找出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;
            }
        }
    }
}
  1. p[] 陣列是儲存對應最短路徑下標 D[] 陣列用於儲存源點到各點最短路徑的權值和 final[] 初始化為0 用來表示未知的狀態。
  2. 將D[] 初始化成為v0 的鄰接矩陣,找到D 陣列中最小值,相當於在與v0 所連線的點中,找到距離最小的,將對應的final數組裡的元素置1,然後根據這個找到的點(相當於該點作為前驅)找與該點連線的頂點 並且根據較短距離原則來更新D陣列。
  3. 作為前驅點記錄下標 更新記錄在P 數組裡面
  4. 下面每一步 都是在前面找到最短的基礎上再來尋找最小值的。
  5. 最終 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)

  1. 關於程式碼:
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");
    }
}
  1. 基本思想也是通過找最小路徑的問題 很巧妙通過 中間點的轉換 進行更新鄰接矩陣 例如開始的時候 是從V0–>V2,為5 的路徑,若是從V0–>V1–>V2,距離為3 則需要替代。
  2. 相當於將Dijkstra Algorithm 進行升級 成從任意點到任意點的最小路徑,當然前面演算法的D陣列和 P陣列自然就變成二維陣列 進行路徑和權重的記錄。
  3. 通過中間點找到更短值的就進行替換。最初的 D − 1 D^{-1} D1,經過一輪輪的跟新到 D 0 , D 1 , D 2 … … D^{0},D^{1},D^{2} …… D0,D1,D2

05 關於AOV AOE網

  1. AOV (Activity On Vertex Network) 分別表示活動網 有活動的先後順序,對於邊(弧)則代表著一種制約關係,具有順序。
  2. AOE (Activity On Edge Network) 相當於弧長值 帶有活動進行的時間的活動網。

5.1 拓撲排序

  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;
}
  1. 先轉化到鄰接表(頂點帶有入度)的形式,進行迴圈 將所有入度為0 的頂點進行堆疊 PUSH操作,然後進行POP操作,將POP出的點 將與之所相連的頂點的入度-1,判斷入度是否為0,是PUSH。(這就相當於 需要把本點前面的事情都做完 之後,才能來到這個節點 進行操作),相當於將 入度為0 的。

5.2 關鍵路徑

  1. 應該是難理解的一個演算法了
  2. 拓撲排序講的是解決一個工程能否順利進行的問題,而這裡我們還需要關心 總的完成時間問題。理解為最關鍵的問題,每個把步驟最長的時間,就很緊密 中間無休息的那種,成為關鍵步驟。把具有最大長度的路徑(時間),才是我們要找的關鍵路徑。
  3. 理解幾個引數etv(earliest time of vertex) 事件的最早發生時間 ==>ltv;
  4. 還有ete(earliest time of edge) 活動最早開始時間 ==> lte
  5. 求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;
}
  1. 求關鍵路徑程式碼:
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);
            }
        }
    }
}
  1. 更新etv[] 就是找出最長(最耗時的步驟)作為最早發生時間,就是從源點到該點所走路徑達到的最大時間,就是從V0在走其它路徑到這兒的話,用的時間都是 <= 最大時間。
  2. 同時在更新ltv[] 時候 ,對應於另一件事情 (<=最大時間的) 最短的時間 min(用最長時間 - 邊的權重值(活動時間))
  3. 舉個例子: etv[1] =3 ,ltv[1] =7 ,v1 這個時間最早也只能在第三天開始(按照總的工程進度來說話),同時 v1也可以在第七天開始 也可以不影響工期的完成。說明中間有四天的靈活時間,(像你是先寫作業 還是先玩是一樣的事情),如果靈活時間沒有 ,則此時就是關鍵的路徑了 時間緊湊的感覺。
  4. 活動的最早發生時間ete 就像當於 etv(事件的最早發生時間),活動的最遲發生時間lte[] 就是求最小的發生時間 相當於 最遲的發生時間(你這條路線再不去做的話,就是你耽誤工程時間)(這一段 還需要細細理解 細細去品 !!!)

總結

  1. 第二遍去學習這個演算法,然而第一遍當時挺明白,在第二遍的時候,又有重頭再來的感覺,邏輯重新整理,又再次進入兩天出來,所以這次要把學到的,看到的,記錄下來 記錄自己現在所看到的東西,於是用一天的時間來記錄。
  2. 暫且把這當做過程吧! 慢啊慢啊 這應該就是學習。