數據結構與算法--圖的應用拓撲排序 & 關鍵路徑

數據結構與算法--圖的應用拓撲排序 & 關鍵路徑

1. 拓撲排序

  • 拓撲排序簡介

    假設在如下面的一張 有向圖 中,頂點表示活動,弧表示活動之間的優先關係,這樣的 有向圖 頂點表示活動網,我們稱之爲 AOV 網。

  上面 AOV 網的路徑爲:V1 -> V2 -> V3 -> V4 -> V5 或者 V1 -> V2 -> V3 -> V4 -> V5

G = (V,E)是一個具有 n個 頂點的 有向圖,V 中的頂點V1、V2...Vn,若滿足從頂點 ViVj 有一條路徑,則在頂點序列Vi必須在Vj之前,則我們稱這樣的頂點序列爲 拓撲排序

所謂 拓撲排序,其實是對一個有向圖構造拓撲序列的過程。

構造過程拓撲序列會產生兩個結果:

1. 如果此網中的全部頂點被輸出,說明是不存在環(迴路)的AOV 網
2. 如果輸出頂點小於總頂點數,說明是存在環(迴路)的,不是AOV 網
  • 拓撲排序實現


    如上的一個AOV 網圖,
    那麼AOV 網圖怎麼存儲比較合適呢?

    我們可以設計一個 鄰接表來存儲,鄰接表節點中增加一個變量,用來存儲頂點的入度,鄰接表節點結構如下:

頂點表結點

//頂點表結點
typedef struct VertexNode
{
    //頂點入度
    int in;
    //頂點域,存儲頂點信息
    int data;
    //邊表頭指針
    EdgeNode *firstedge;
}VertexNode, AdjList[MAXVEX];

圖結構:

//圖結構
typedef struct
{
    AdjList adjList;
    //圖中當前頂點數和邊數
    int numVertexes,numEdges;
}graphAdjList,*GraphAdjList;

思路分析:

1. 利用棧的特性,先將圖網中所有入度爲 0 的頂點下標入棧,
2. 循環遍歷棧,直到棧爲空
3. 棧頂元素出棧,並遍歷與棧頂相連接的弧
   3.1 獲取與棧頂元素對應頂點連接的頂點
   3.2 將與棧頂元素對應頂點連接的頂點入度減一
   3.3 判斷入度是否爲 0 ,爲 0 則入棧

先定義鄰接矩陣鄰接表

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXEDGE 20
#define MAXVEX 14
#define INFINITYC 65535

/* Status是函數的類型,其值是函數結果狀態代碼,如OK等 */
typedef int Status;

// 鄰接矩陣
typedef struct {
    int vexs[MAXVEX];
    int arc[MAXVEX][MAXVEX];
    int numVertexes, numEdges;
}MGraph;
// 鄰接表結構
// 邊表節點
typedef struct EdgeNode{
    int adjvex; // 鄰接頂點域,存儲頂點的下標
    int weight; // 權值
    struct EdgeNode *next; // 鏈域,存儲下一個鄰接點
}EdgeNode;

// 頂點表
typedef struct VertexNode{
    int inCount;  // 入度數
    int data;     // 頂點信息
    EdgeNode *firstEdge;  // 邊表頭指針
}VertexNode, AdjList[MAXVEX];


// 圖結構
typedef struct {
    AdjList adjList;
    int numVertexes, numEdges;
}graphAdiList, *GraphAdjList;

由於一個一個的輸入圖的頂點和邊,比較麻煩,我們定義一個函數,來構建鄰接矩陣的圖,然後轉換爲鄰接表

// 1.構成AOV網圖
void CreateMGraph(MGraph *G)/* 構件圖 */
{
    int i, j;
    G->numVertexes = MAXVEX;
    G->numEdges    = MAXEDGE;
    
    for (i = 0; i < G->numVertexes; i++){
        G->vexs[i] = i;
    }
    
    // 初始化圖
    for (i = 0; i < G->numVertexes; i++){
        for (j = 0; j < G->numVertexes; j++){
            G->arc[i][j] = 0;
        }
    }
    
    G->arc[0][4] =1;
    G->arc[0][5] =1;
    G->arc[0][11]=1;
    G->arc[1][2] =1;
    G->arc[1][4] =1;
    G->arc[1][8] =1;
    G->arc[2][5] =1;
    G->arc[2][6] =1;
    G->arc[2][9] =1;
    G->arc[3][2] =1;
    G->arc[3][13]=1;
    G->arc[4][7] =1;
    G->arc[5][8] =1;
    G->arc[5][12]=1;
    G->arc[6][5] =1;
    G->arc[8][7] =1;
    G->arc[9][10]=1;
    G->arc[9][11]=1;
    G->arc[10][13]=1;
    G->arc[12][9] =1;
}

// 2.將AOV網圖藉助鄰近矩陣轉換成鄰接表結構
void CreateALGraph(MGraph G,GraphAdjList *GL)
{
    int i,j;
    EdgeNode *e;
    
    // 創建圖
    (*GL) = (GraphAdjList)malloc(sizeof(graphAdiList));
    // 對邊和頂點賦值
    (*GL)->numEdges = G.numEdges;
    (*GL)->numVertexes = G.numVertexes;
 
    
    // 讀入頂點表
    for (i = 0; i < G.numVertexes; i++) {
        (*GL)->adjList[i].inCount = 0;
        (*GL)->adjList[i].data    = G.vexs[i];
        (*GL)->adjList[i].firstEdge = NULL;
    }
    
    for (i = 0; i < G.numVertexes; i++) {
        for (j = 0; j < G.numVertexes; j++) {
            if (G.arc[i][j]) {
                // 創建邊表節點
                e = (EdgeNode *)malloc(sizeof(EdgeNode));
                //鄰接序號爲j
                e->adjvex = j;
                // 將當前頂點上的指向的結點指針賦值給e
                e->next = (*GL)->adjList[i].firstEdge;
                // 將當前頂點的指針指向e
                (*GL)->adjList[i].firstEdge = e;
                // 入度加1
                (*GL)->adjList[j].inCount++;
            }
        }
    }
    
}

接下來就是關鍵的拓撲排序

// 若AOV網圖無迴路則輸出拓撲排序的序列並且返回狀態值1,若存在迴路則返回狀態值0// 拓撲排序:解決的是一個工程能否順序進行的問題
Status TopologicalSort(GraphAdjList GL){
    EdgeNode *e;
    int i, k, getTop;
    int top  = 0;  // 棧頂下標
    int count = 0;// 統計頂點個數
    
    // ✅ 創建棧
    int *stack = (int *)malloc(sizeof(int) * GL->numVertexes);
    
    // ✅ 遍歷入度爲0的頂點入棧
    for (i = 0; i < GL->numVertexes; i++) {
        if (GL->adjList[i].inCount == 0) {
            stack[++top] = i;
        }
    }
    printf("top = %d\n",top);
    
    // ✅ 循環遍歷棧
    while (top != 0) {
        // ✅ 出棧棧頂元素
        getTop = stack[top--];
        printf("%d -> ",GL->adjList[getTop].data);
        //輸出頂點,並計數
        count++;
        // ✅ 遍歷與棧頂相連接的弧
        for (e = GL->adjList[getTop].firstEdge; e; e = e->next) {
            // ✅ 獲取與gettop連接的頂點
            k = e->adjvex;
            // ✅ 將與gettop連接的頂點入度減1.判斷如果當前減1後爲0,則入棧
            if (!(--GL->adjList[k].inCount)) {
                //將k入棧到stack中,並且top加1;
                stack[++top]=k;
            }
        }
    }
    
    // ✅ 判斷是否把所有的頂點都輸出. 則表示找到了拓撲排序;
    if(count < GL->numVertexes)
        return ERROR;
    else
        return OK;
}

2. 關鍵路徑

在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,邊的權重表示活動的持續事件,這種有向圖的邊表表示活動的網,稱之爲AOE網

沒有入邊的頂點稱之爲始點或源點
沒有出邊的頂點稱之爲終點或匯點,由於一個工程,總有一個開始一個結束,所以正常情況下,AOE網只有一個源點和匯點

比圖下面的AOE網

在AOE網中:

  • 路徑上各個活動所持續的時間之和,稱爲路徑長度
  • 從源點到匯點具有最大的路徑叫關鍵路徑
  • 在關鍵路徑上的活動叫關鍵活動

那麼怎麼求解一個 AOE網 的關鍵路徑呢?接下來看一下,關鍵路徑求解過程中的幾個核心參數。

  • 事件最早發生的時間 etv:即頂點Vk的最早發生時間
  • 事件最晚發生的時間 ltv:即頂點Vk的最晚發生時間,也就是每個頂點對應的事件最晚需要開始的時間,超出此時間將會延誤整個工期
  • 活動的最早開工時間**ete**:即弧Ak的最早發生時間
  • 活動的最晚開工時間**lte**:即弧Ak的最晚發生時間(不推遲工期的最晚開工時間)

求事件的最早發生時間 etv的過程,就是從頭到尾去找拓撲序列的過程,所以在求解關鍵路徑之前,我們需要調用一次拓撲排序的序列去計算etv和拓撲序列列表

對上面的AOE網圖etv的求解分析 如下:

1. 從源點 V0->V1,a0 = 3,V0->V2,a1 = 4,所以etv[1] = 3,etv[2] = 4
2. V1->V3,a2 = 5,V2->V3,a4 = 8,所以etv[3] = max{3+5,4+8} = 12

由此可以推演出etv的計算公式:

對上面的AOE網圖ltv的求解分析 如下:

首先將 ltv 初始化成 etv 最後一個事件的時間。(上圖最終求得etv[9] = 27)
1. 最後一個事件V9,沒有弧表(因爲是匯點),沒有要更新的相關 ltv
2. V8->V9 長度爲3,所以 ltv = min(27, 27-3) = 24,ltv[8] = 24
3. V7->V8 長度爲5,所以 ltv = min(24, 24-5) = 19,ltv[7] = 19

依次類推最終可以推演出ltv的計算公式

代碼實現:

// 定義鄰接矩陣、鄰接表、圖
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

#define MAXEDGE 30
#define MAXVEX 30
#define INFINITYC 65535

typedef int Status;    /* Status是函數的類型,其值是函數結果狀態代碼,如OK等 */

// 鄰接矩陣
typedef struct {
    int vexs[MAXVEX];
    int arc[MAXVEX][MAXVEX];
    int numVertexes, numEdges;
}MGraph;
// 鄰接表
typedef struct EdgeNode{
    int adjvex; // 鄰接點域,存儲頂點下標
    int weight; // 權重
    struct EdgeNode *next; // 鏈域,下一個鄰接點
    
}EdgeNode;

typedef struct VertexNode{
    int in; // 入度
    int data; // 頂點域,頂點信息
    EdgeNode *firstEdge;  // 邊表頭指針
}VertexNode, AdjverList[MAXVEX];


// 圖
typedef struct {
    AdjverList adjList;  // 頂點表
    int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;


// ✅ 通過鄰接矩陣將AOE網圖轉換爲鄰接表
void CreateMGraph(MGraph *G)/* 構件圖 */
{
    int i, j;
    // 頂點數和邊數
    G->numEdges = 13;
    G->numVertexes = 10;
    
    // 初始化圖
    for(i = 0; i < G->numVertexes; i++){
        G->vexs[i] = i;
    }
    
    for(i = 0; i < G->numVertexes; i++){
        for(j = 0; j < G->numVertexes; j++){
            if(i == j){
                G->arc[i][j] = 0;
            }else {
                G->arc[i][j]=INFINITYC;
            }
        }
    }
    
    G->arc[0][1]=3;
    G->arc[0][2]=4;
    G->arc[1][3]=5;
    G->arc[1][4]=6;
    G->arc[2][3]=8;
    G->arc[2][5]=7;
    G->arc[3][4]=3;
    G->arc[4][6]=9;
    G->arc[4][7]=4;
    G->arc[5][7]=6;
    G->arc[6][9]=2;
    G->arc[7][8]=5;
    G->arc[8][9]=3;

}

void CreateALGraph(MGraph G,GraphAdjList *GL){
    int i, j;
    EdgeNode *e;
    
    *GL = (GraphAdjList)malloc(sizeof(graphAdjList));
    (*GL)->numVertexes = G.numVertexes;
    (*GL)->numEdges = G.numEdges;
    
    // 讀取頂點表
    for (i = 0; i < (*GL)->numVertexes; i++) {
        (*GL)->adjList[i].in = 0;
        (*GL)->adjList[i].data = G.vexs[i];
        (*GL)->adjList[i].firstEdge = NULL;
    }
    
    // 讀取邊表
    for (i = 0; i < (*GL)->numVertexes; i++) {
        for (j = 0; j < (*GL)->numVertexes; j++) {
            if (G.arc[i][j] != 0 && G.arc[i][j] < INFINITYC) {
                e = (EdgeNode *)malloc(sizeof(EdgeNode));
                e->adjvex = j;
                e->weight = G.arc[i][j];
                
                e->next = (*GL)->adjList[i].firstEdge;
                (*GL)->adjList[i].firstEdge = e;
                (*GL)->adjList[j].in++;
            }
        }
    }
}

// ✅ 定義全局變量
int *etv,*ltv; /* 事件最早發生時間和最遲發生時間數組,全局變量 */
int *stack2;   /* 用於存儲拓撲序列的棧 */
int top2;       /* 用於stack2的指針*/

// ✅ 拓撲排序
Status TopologicalSort(GraphAdjList GL){
    
    int i, k, getTop;
    EdgeNode *e;
    int top = 0;
    int count = 0;
    
    // 初始化棧
    int *stack = (int *)malloc(sizeof(int) * GL->numVertexes);
    // 入度爲0的頂點入棧
    for (i = 0; i < GL->numVertexes; i++) {
        if (GL->adjList[i].in == 0) {
            stack[++top] = i;
        }
    }
    // 創建初始化拓撲序列棧
    top2 = 0;
    stack2 = (int *)malloc(sizeof(int) * GL->numVertexes);
    
    // 事件最早發生時間數組
    etv = (int *)malloc(sizeof(int) * GL->numVertexes);
    for (i = 0 ; i < GL->numVertexes; i++) {
        //初始化
        etv[i] = 0;
    }
    
    while (top != 0) {
        // 出棧
        getTop = stack[top--];
        // 打印出棧頂點
        printf("%d -> ", GL->adjList[getTop].data);
        // 計數+1
        count++;
        
        // 將出棧的壓入拓撲序列棧
        stack2[++top2] = getTop;
        
        for (e = GL->adjList[getTop].firstEdge; e; e = e->next) {
            k = e->adjvex;
            if (!(--GL->adjList[k].in)) {
                stack[++top] = k;
            }
            
            //求各頂點事件的最早發生的時間etv值
            if ((etv[getTop] + e->weight) > etv[k]) {
                etv[k] = etv[getTop] + e->weight;
            }
        }
    }
    
    
    if(count < GL->numVertexes)
        return ERROR;
    else
        return OK;
    return OK;

}

// ✅ 求關鍵路徑, GL爲有向網,則輸出G的各項關鍵活動;
void CriticalPath(GraphAdjList GL){
    
    EdgeNode *e;
    int i, j, k, getTop;
    // 聲明活動最早和最晚發生時間
    int ete, lte;
    
    //求得拓撲序列,計算etv數組以及stack2的值
    TopologicalSort(GL);
    
    printf("etv:\n");
    for(i = 0; i < GL->numVertexes; i++){
        printf("etv[%d] = %d \n",i,etv[i]);
    }
           
    printf("\n");
    
    //事件最晚發生時間數組
    ltv = (int *)malloc(sizeof(int) * GL->numVertexes);
    
    // 初始化ltv數組
    for (i = 0; i < GL->numVertexes; i++) {
        ltv[i] = etv[GL->numVertexes-1];
    }
    
    //計算ltv(事件最晚發生時間) 出棧求ltv
    while (top2 != 0) {
        // 出棧
        getTop = stack2[top2--];
        for (e = GL->adjList[getTop].firstEdge; e; e = e->next) {
            k = e->adjvex;
            if ((ltv[k] - e->weight) < ltv[getTop]) {
                ltv[getTop] = ltv[k] - e->weight;
            }
        }
    }
    
    //打印ltv 數組
    printf("ltv:\n");
    for (i = 0 ; i < GL->numVertexes; i++) {
        printf("ltv[%d] = %d \n",i,ltv[i]);
    }
    
    // 求解ete,lte 並且判斷lte與ete 是否相等.相等則是關鍵活動;
    for (j = 0; j < GL->numVertexes; j++) {
        for (e = GL->adjList[j].firstEdge; e; e = e->next) {
            k = e->adjvex;
            ete = etv[j];
            lte = ltv[k] - e->weight;
            if (ete == lte) {
                printf("<%d-%d> length:%d\n",GL->adjList[j].data, GL->adjList[k].data, e->weight);
            }
        }
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章