第 7 章 圖

7.1 開場白

7.2 圖的定義

圖(Graph)是有頂點的有窮非空集合和頂點之間邊的集合組成,通常表示爲:G(V,E),其中,G表示一個圖,V 是圖G中頂點的集合,E是圖G中邊的集合。
注:在定義中,若V是頂點的集合,則強調了頂點集合V有窮非空。
在圖中,任意兩個頂點之間都可能有關係,頂點之間的邏輯關係用變來表示,邊集可以是空的。

7.2.1 各種圖定義

  無向邊:若頂點vivj 之間的邊沒有方向,則稱這條邊是無向邊(Edge),用無序偶對(vivj )表示。如果圖中任意兩個頂點之間的邊都是無向邊,則稱該圖是無向圖
  有向邊:若從頂點vivj 的邊有方向,則稱這條邊爲有向邊,也稱弧(Arc)。用有序偶對<vivj >,vi 稱爲弧尾(Tail),vj 稱爲弧頭(Head)。如果圖中任意兩個頂點之間的邊都是有向邊,則稱該圖爲有向圖
  在圖中,若不存在頂點到其自身的邊,其同一條邊不重複出現,則稱這樣的圖爲簡單圖
  在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖爲無向完全圖。含有 n 個結點的無向完全圖有n(n+1)2 條邊。
  在有向圖中,如果任意兩個頂點之間都存在方向互爲相反的兩條弧,則稱該圖爲有向完全圖。含有 n 個頂點的有向完全圖有 n*(n-1)條邊。
  有很少條邊或弧的圖稱爲稀疏圖,反之稱爲稠密圖
  有些圖的邊或弧具有與它相關的數字,這種與圖的邊或弧相關的樹叫做權(Weight)。這種帶權的圖通常稱爲(Network)。
  假設有兩個圖G=(V,{E})和G’=(V’,{E’}),如果V’⊆ V,且E’⊆ E,則稱G’爲G的子圖(Subgraph).
  

7.2.2 圖的頂點與邊間的關係

  對於無向圖G=(V,{E}),如果邊(v,v’)∈E,則成頂點v和v’互爲鄰接點(Adjacent),即v和v’相鄰接。邊(v,v’)依附於頂點v和v’,或者說(v,v’)與頂點v和v’相關聯。頂點的度(degree)是和v相關聯的邊的數目,記爲TD(V)。推論得到:邊數其實就是各定點度數和的一半。
  對於有向圖G=(V,{E}),如果弧 < v,v’>∈E,則稱頂點v鄰接到頂點v’,頂點v’鄰接自頂點v。弧< v,v’>和頂點v,v’相關聯。以頂點v爲頭的弧的數目稱爲v的入度(InDegree),記爲ID(v);以v爲尾的弧的數目稱爲v的出度(OutDegree),記爲OD(v);頂點v的度爲TD(v)=ID(v)+OD(v)。
  無向圖G=(V,{E})中從頂點v到頂點v’的路徑(Path)是一個頂點序列(v=vi,0,vi,1,...,vi,m=v ,其中,(vi,j1,vi,j)E ,1<= j <= m.路徑的長度是路徑上的邊或弧的數目。第一個頂點到最後一個頂點相同的路徑稱爲迴路或環。序列中頂點不重複出現的路徑稱爲簡單路徑。除了第一個頂點和最後一個頂點之外,其餘頂點不重複出現的迴路,稱爲簡單迴路或簡單環
  

7.2.3 連通圖相關術語

  在無向圖G中,如果從頂點v到頂點v’有路徑,則稱v和v’是連通的。如果對於圖中任意兩個頂點vivjVvivj 都是連通的,則稱G是連通圖
  無向圖中的極大連通子圖稱爲連通分量。注意連通分量的概念,它強調:
  - 要是子圖;
  - 子圖要是連通的;
   - 連通子圖含有極大頂點數;
   - 具有極大頂點數的連通子圖包含依附於這些頂點的所有邊。
  
  在有向圖G中,如果對於每一對vivjVvi!=vj ,從vivj 和從vjvi 都存在路徑,則稱G是強連通圖。有向圖中的極大強連通子圖稱作有向圖
  所謂的一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的 n 個頂點,但只有足以構成一棵樹的 n-1條邊。推導的:如果一個圖有n個頂點和小於n-1條邊,則是非連通圖;如果它多於n-1邊條,必定構成一個環。但是,有n-1條邊不一定會生成樹。
  如果一個有向圖恰有一個頂點的入度爲0,其餘頂點的入度均爲1,則是一棵有向樹。一個有向圖的生成森林由若干棵有向樹組成,含有圖中全部頂點,但只有足以構成若干棵不相交的有向樹的弧。
  

7.2.4 圖的定義與術語總結

7.3 圖的抽象數據類型

ADT 圖(Graph)
Data
    頂點的有窮非空集合和邊的集合
Operation
    CreateGraph(*G,V,VR): 按照頂點集V和弧集的定義構造圖G。
    DestroyGraph(*G):圖G存在則銷燬。
    LocateVex(G,u):若圖G中存在頂點u,則返回圖中的位置。
    GetVex(G,v):返回圖G中頂點v的值。
    PutVex(G,v,value):將圖G中頂點v賦值給value。
    FirstAdjVex(G,*v):返回頂點v的一個鄰接頂點,若頂點在G中無鄰接頂點返回空。
    NextAdjVex(G,v,*w):返回頂點v相對於頂點w的下一個鄰接頂點,若w是v的最後一個鄰接點,則返回空。
    InsertVex(*G,v):在圖G中增添新頂點v。
    DeleteVex(*G,v):刪除圖G中頂點v及其相關的弧。
    InsertArc(*G,v,w):在圖G中增添弧<v,w>,若G是無向圖,還需要增添對稱弧<w,v>。
    DeleteArc(*G,v,w):在圖中刪除弧<v,w>,若G是無向圖,還需要刪除對稱弧<w,v>。
    DFSTraverse(G):對圖G中進行深度優先遍歷,在遍歷過程中對每個頂點調用。
    HFSTraverse(G):對圖G中進行廣度優先遍歷,在遍歷過程對每個頂點調用。
endADT

7.4 圖的存儲結構

五種不同的存儲結構。

7.4.1 鄰接矩陣

圖的鄰接矩陣(Adjacency Matrix)存儲方式是用兩個數組表示圖。一個一維數組存儲圖中頂點信息,一個二維數組(稱爲鄰接矩陣)存儲圖中的邊或弧的信息。

typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535

typedef struct
{
    VertexType  vexs[MAXVEX];    //頂點表
    EdgeType    arc[MAXVEX][MAXVEX];    //鄰接矩陣,可看做表
    int         numVertexes, numEdges;  //圖中當前的頂點數和邊數
}MGraph;

//建立無向網圖的鄰接矩陣表示
void CreateMGraph(MGraph *G)
{
    int i, j, k, w;
    printf("輸入頂點數和邊數:\n");
    scanf("%d,%d",&G->numVertexes, &G->numEdges);
    for (i = 0; i < G->numVertexes; ++i)    //讀入定點信息,建立頂點表
        scanf(&G->vexs[i]);
    for (i = 0; i < G->numVertexes; ++i)
        for (j = 0; j < G->numVertexes; ++j)
            G->arc[i][i] = INFINITY;        //鄰接矩陣初始化
    for (k = 0; k < G->numEdges; ++k)
    {
        printf("輸入邊(vi,vj)上的下標i和下標j和權 w:\n");
        scanf("%d,%d,%d,", &i, &j, &w);
        G->arc[i][j] = w;
        G->arc[j][i] = G->arc[i][j];
    }
}

從圖中可以看到,n個頂點e條邊的無向網圖的創建,時間複雜度爲O(n+n2+e ,其中對鄰接矩陣G.arc的初始化花費了O(n2 )的時間。

7.4.2 鄰接表

鄰接矩陣對於邊數相對頂點數較少的圖,存在存儲空間的極大浪費。
把數組與鏈表相結合的方法稱爲鄰接表(Adjacency List)
鄰接表的處理辦法如下:

  1. 圖中頂點用一個一維數組存儲,當然,頂點也可以用鏈表來存儲,不過數組可以較容易第讀取頂點信息,更加方便。另外,對於頂點數組中,每個數據元素還需要存儲指向第一個鄰接點的指針,以便於查找該頂點的邊信息。
  2. 圖中每個頂點vi的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所有用單鏈表表示,無向圖稱爲頂點vi的邊表,有向圖稱爲頂點vi作爲弧尾的出邊表。對於帶權值的圖,可以在邊表結點定義中再增加一個weight的數據域,存儲權值信息即可。

一個有向圖的逆鄰接表:即對每個頂點vi都建立一個鏈接爲vi爲弧頭的表。

typedef char VertexType;
typedef int  EdgeType;

typedef struct EdgeNode     //邊表結點
{
    int         adjvex;     //鄰接點域,存儲該頂點對應的下標
    EdgeType    weight;     //用於存儲權值
    struct EdgeNode *next;  //鏈域,指向下一個鄰接點
}EdgeNode;

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

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

//建立圖的鄰接表結構
void CreateALGraph(GraphAdjList *G)
{
    int i, j, k;

    EdgeNode *e;
    printf("輸入頂點數和邊數:\n");
    scanf("%d, %d",&G->numVertexes, &G->numEdges);  //輸入定點數和邊數
    for (i = 0; i < G->numVertexes; ++i)        //讀入頂點信息,建立頂點表
    {
        scanf(&G->AdjList[i].data);             //輸入頂點信息
        G->adjList[i].firstedge = NULL;         //將邊表置爲空表
    }
    for (k = 0; k < G->numEdges; ++k)           //建立邊表
    {
        printf("輸入邊(vi,vj)上的頂點序號:\n");
        scanf("%d,%d,%d", &i,&j);
        e = (EdgeNode *)malloc(sizeof(EdgeNode));   //向內存申請空間生成邊表結點
        e->adjvex = j;                              //鄰接序號爲j
        e->next = G->adjList[i].firstedge;      //將e指針指向當前頂點指向的結點
        G->adjList[i].firstedge = e;            //將當前頂點的指針指向e

        e = (EdgeNode *)malloc(sizeof(EdgeNode));   //向內存申請空間生成邊表結點
        e->adjvex = i;                              //鄰接序號爲i
        e->next = G->adjList[j].firstedge;      //將e指針指向當前頂點指向的結點
        G->adjList[j].firstedge = e;            //將當前頂點的指針指向e

    }
}

7.4.3 十字鏈表

鄰接表是有缺陷的,想了解入度就必須要遍歷整個表才知道。反之,逆鄰接表解決了入度卻解決不了出度。
把鄰接表和逆鄰接表結合起來,整合在一起就是:十字鏈表
這裏寫圖片描述
這裏寫圖片描述

7.4.4 鄰接多重表

對於無向圖的鄰接表,刪除一條邊時,需要刪除兩個結點,比較麻煩。仿照十字鏈表的方式,對邊表結點的結構進行一些改造。
重新定義的邊表結點結構如圖
這裏寫圖片描述
其中,ivex和jvex是與某條邊依附的兩個頂點在頂點表的下標。ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一邊。這就是鄰接多重表結構
這裏寫圖片描述
這裏寫圖片描述

7.4.5 邊集數組

邊集數組是由兩個一維數組構成。一個是存儲頂點的信息;另一個是存儲邊的信息,這個邊數組每個數據元素有一條邊的起點下標(begin)、終點下標(end)和權(weight)組成。顯然邊集數組關注的是邊的集合,在邊集數組中要查找一個頂點的度需要掃面整個邊數組,效率並不高。因此它更適合對邊依次進行處理的操作,而不適合對頂點相關的操作。

7.5 圖的遍歷

從圖中某一頂點出發訪遍圖中其餘頂點,且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。

7.5.1 深度優先遍歷(Depth_First_Search)

也稱爲深度優先搜索,簡稱爲(DFS)。它從圖中某個頂點v出發,訪問此頂點,然後從v的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和v有路徑相通的頂點都被訪問到。若圖中尚有頂點未被訪問,則另選圖中一個未曾被訪問的頂點做起始點,重複上述過程,直至圖中所有頂點都被訪問到爲止。
如果用的是鄰接矩陣的方式,則代碼如下:

//用鄰接矩陣的方式
typedef int Boolean;    //Boolean是布爾類型,其值是TRUE或FALSE
Boolean visited[MAX];   //訪問標誌的數組
//鄰接矩陣的深度優先遞歸算法
void DFS(MGraph 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] == 1 && !visited[j])
            DFS(G,j);       //對未訪問的鄰接頂點遞歸調用
}
//鄰接矩陣的深度遍歷操作
void DFSTraverse(MGraph G)
{
    int i;
    for (i = 0; i < G.numVertexes; ++i)
        visited[i] = FALSE;     //初始所有頂點狀態都是未訪問過狀態
    for (i = 0; i < G.numVertexes; ++i)
        if (!visited[i])    //對未訪問的鄰接頂點遞歸調用DFS,若是連通圖,只會執行一次
            DFS(G, i);
}

如果圖結構是鄰接表結構,函數代碼幾乎相同,只是在遞歸函數中因爲將數組換成了鏈表而有不同,代碼如下:

//鄰接表的深度優先遞歸算法
void DFS(GraphAdjList GL, int i)
{
    EdgeNode *p;
    visited[i] = TRUE;
    printf("%c", GL->adjList[i].data);  //打印頂點,也可以其他操作
    p = GL->adjList[i].firstedge;
    while (p)
    {
        if (!visited[p->adjvex])
            DFS(GL, p->adjvex); //對未訪問的鄰接頂點遞歸調用
        p = p->next;
    }
}

//鄰接表的深度遍歷操作
void DFSTraverse(GraphAdjList GL)
{
    int i;
    for (i = 0; i < GL->numVertexes; ++i)
        visited[i] = FALSE; //初始所有頂點狀態都是未訪問過狀態
    for (i = 0; i< GL->numVertexes; ++i)
        if (!visited[i])    //對未訪問的鄰接頂點遞歸調用DFS,若是連通圖,只會執行一次
            DFS(GL, i);
}

對比兩個不同存儲結構的深度優先遍歷算法,對於n個頂點e條邊的圖來說,鄰接矩陣由於是二維數組,要查找每個頂點的鄰接點需要訪問矩陣中的所有元素,因此都需要O(n2 的時間,而鄰接表做存儲結構時,找鄰接點所需的時間取決於頂點和邊的數量,所以是O(n+e)。顯然對於點多變少的稀疏圖來說,鄰接表結構使得算法在實踐效率上大大提高。

7.5.2 廣度優先遍歷(Breadth_First_Search)

又稱爲廣度優先搜索,簡稱BFS。類似與樹的層序遍歷。

//鄰接矩陣的廣度遍歷算法
void BFSTraverse(MGraph G)
{
    int i, j;
    Queue Q;
    for (i = 0; i < G.numVertexes; ++i)
        visited[i] = FALSE;
    InitQueue(&Q);              //初始化一輔助用的隊列
    for (i = 0; i < G.numVertexes; ++i) //對每一個頂點做循環
    {
        if ( !visited[i])   //若是未訪問過就處理
        {
            visited[i] = TRUE;  //設置當前頂點訪問過
            printf("%c", G.vexs[i]);    //打印頂點,也可以其他操作
            EnQueue(&Q, i);         //將次頂點如隊列
            while (!QueueEmpty(Q))  //若當前隊列不爲空
            {
                DeQueue(&Q, &i);        //將隊中元素出隊列,賦值給i
                for (j = 0; j < G.numVertexes; ++j)
                {
                    //判斷其他頂點若與當前頂點存在邊且未訪問過
                    if (G.arc[i][j] == 1 && !visited[j])
                    {
                        visited[j] = TRUE; //將找到的次頂點標記爲已訪問
                        printf("%c", G.vexs[j]);    //打印頂點
                        EnQueue(&Q, j);         //將找到的次頂點入隊列
                    }
                }
            }
        }
    }
}

對於鄰接表的廣度優先遍歷,代碼與鄰接矩陣差異不打,代碼如下:

//鄰接表的廣度遍歷算法
void BFSTraverse(GraphAdjList GL)
{
    int i;
    EdgeNode *p;
    Queue Q;
    for (i = 0; i < GL->numVertexes; ++i)
        visited[i] = FALSE;
    InitQueue(&Q);              //初始化一輔助用的隊列
    for (i = 0; i < GL->numVertexes; ++i)   //對每一個頂點做循環
    {
        if ( !visited[i])   //若是未訪問過就處理
        {
            visited[i] = TRUE;  //設置當前頂點訪問過
            printf("%c", G.vexs[i]);    //打印頂點,也可以其他操作
            EnQueue(&Q, i);         //將次頂點如隊列
            while (!QueueEmpty(Q))  //若當前隊列不爲空
            {
                DeQueue(&Q, &i);        //將隊中元素出隊列,賦值給i
                p = GL->adjList[i].firstedge;   //找到當前頂點邊錶鏈表頭指針
                while (p)
                {
                    if (!visited[p->adjvex])    //若此頂點未被訪問
                    {
                        visited[p->adjvex] = TRUE;
                        printf("%c", GL->adjList[p->adjvex].data);
                        EnQueue(&Q, p->adjvex);     //將此頂點入隊列
                    }
                    p = p->next;        //指針指向下一個鄰接點
                }
            }
        }
    }
}

對比圖的深度優先遍歷和廣度優先遍歷算法,它們在時間複雜度上是一樣的,不同之處僅僅對頂點訪問的順序不同。可見兩者在全圖遍歷上是沒有優劣之分,只是視不同的情況選擇不同的算法。
深度優先更適合目標比較明確,已找到目標爲主要目的的情況,而廣度優先更適合在不斷擴大遍歷範圍時找到相對最優解的情況。

7.6 最小生成樹

把構造連通圖網的最小代價生成樹稱爲最小生成樹(Minimum Cost Spanning Tree)。找連通網的最小生成樹,有兩種經典算法,普里姆算法和克魯斯卡爾算法。

7.6.1 普里姆算法(Prim)算法

鄰接矩陣表示

//Prim 算法生成最小生成樹
void MiniSpanTree_Prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];     //保存相關頂點下標
    int lowcost[MAXVEX];    //保存相關頂點間邊的權值
    lowcost[0] = 0;         //初始化第一個權值爲0,即v0加入生成樹。lowcost的值爲0,在這裏就是此下標的頂點已經加入生成樹
    adjvex[0] = 0;          //初始化第一個頂點下標 0
    for (i = 1; i < G.numVertexes; ++i)
    {
        lowcost[i] = G.arc[0][i];       //將v0頂點與之有邊的權值存入數組
        adjvex[i] = 0;                  //初始化都爲v0的下標
    }
    for (i = 1; i < G.numVertexes; ++i)
    {
        min = INFINITY;                 //初始化最小權值爲無窮
                                        //通常設置爲不可能的大數字如32768、65535等
        j = 1; k = 0;
        while (j < G.numVertexes)       //循環全部頂點
        {
            if (lowcost[j] != 0 && lowcost[j] < min)    //如果權值不爲0,且權值小於min
            {
                min = lowcost[j];       //則讓當前權值成爲最小值
                k = j;                  //將當前最小值的下標存入k
            }
            ++j;
        }
        printf("(%d, %d)",adjvex[k], k);        //打印當前頂點邊中權值最小邊
        lowcost[k] = 0;                         //將當前頂點的權值設置爲0,表示此頂點已經完成任務
        for (j = 1; j < G.numVertexes; ++j)     //循環所有頂點
        {
            if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) //若下標爲k頂點各邊權值小於此前這些頂點未被加入生成數權值
            {
                lowcost[j] = G.arc[k][j];       //將較小權值存入lowcost
                adjvex[j] = k;                  //將下標爲k頂點存入adjvex
            }
        }
    }
}

普里姆算法的實現定義:
假設N = (V,{E})是連通網那個,TE是 N 上最小生成樹中邊的集合。算法從U={u0}(u0∈V),TE = {}開始。重複執行下述操作:在所有u∈U, v∈V-U的邊(u,v)∈E中找一條代價最小的邊(u0,v0)併入集合TE,同時v0併入U,直至U=V爲止。此時TE中必有n-1條邊,則T=(V,{TE})爲N的最小生成樹。
由算法代碼中的循環嵌套可得知此算法的時間複雜度爲O(n2 )。

7.6.2 克魯斯卡爾算法(Kruskal)

普里姆算法是以某頂點爲起點,逐步找各頂點上最小權值的邊來構建最小生成樹的。同樣的思路,也可以直接就以邊爲目標去構建,只不過構建時要考慮是否會形成環路而已。此時,用到了圖的存儲結構中的邊集數組結構。以下是edge邊集數組結構的定義代碼:

//邊集數組Edge結構的定義
typedef struct
{
    int begin;
    int end ;
    int weight;
}Edge;

把鄰接矩陣轉化爲邊集數組,並對它們按權值從小到達排序。
其中MAXEDGE爲邊數量的極大值,MAXVEX爲頂點個數最大值,代碼如下:

//Kruskal算法生成最小生成樹
void MiniSpanTree_Kruskal(MGraph G) //生成最小生成樹
{
    int i, n, m;
    Edge edges[MAXEDGE];    //定義邊集數組
    int parent[MAXVEX];     //定義一個數組用來判斷邊與邊是否形成環路
    //此處省略將鄰接矩陣G轉化爲邊集數組edges並按權由小到大排序的代碼
    for (i = 0; i < G.numVertexes; ++i)
        parent[i] = 0;
    for (i = 0; i < G.numEdges; ++i)    //循環每一條邊
    {
        n = Find(parent, edges[i].begin);
        m = Find(parent, edges[i].end);
        if (n != m)     //假如n與m不等,說明此邊沒有與現有生成樹形成環路
        {
            parent[n] = m;  //將此邊的結尾頂點放入下標爲起點的parent中,表示此頂點已經在生成樹集合中
            printf("(%d, %d) %d", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
}

int Find(int *parent, int f)        //查找連線頂點的尾部下標
{
    while (parent[f] > 0)
        f = parent[f];
    return f;
}

把克魯斯卡爾算法的實現定義歸納一下結束這一節的講解。
假設N=(V,{E})是連通網,則令最小生成樹的初始狀態爲只有n個頂點而無邊的非連通圖T={V,{}},圖中每個頂點自成一個連通分量。在E中選擇代價最小的邊,若邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中,否則捨去此邊而選擇下一條邊代價最下的邊。依此類推,直至T中所有頂點都在同一連通分量上爲止。
此算法的Find函數由邊數e決定,時間複雜度爲O(loge),而外面有一個for 循環e次。所以克魯斯卡爾算法的時間複雜度爲O(eloge)。
對比兩個算法,克魯斯卡爾算法主要是針對邊來展開,邊數少時效率會非常高,所以對於稀疏圖有很大的優勢;而普里姆算法對於稠密圖,即邊數非常多的情況會更好一些。

7.7 最短路徑

對於非網圖,由於邊上沒有權值,所謂最短路徑,就是值兩頂點之間經過的邊數最少的路徑;而對於網圖來說,最短路徑是指兩頂點之間經過邊上的權值之和最少的路徑,並且稱路徑上的第一個頂點是源點,最後一個頂點是終點

7.7.1 迪傑斯特拉(Dijkstra)算法

這是一個按路徑長度遞增的次序產生最短路徑的算法。它的思路大體是這樣的:
它並不是一下子就求出了v0到v8的最短路徑,而是一步步求出它們之間頂點的最短路徑,過程中都是基於已經求出的最短路徑的基礎上,求得更遠頂點的最短路徑,最終得到你要的結果。

//Dijkstra算法
#define MAXVEX      9
#define INFINITY    65535
typedef int Patharc[MAXVEX];        //用於存儲最短路徑下標的數組
typedef int ShortPathTable[MAXVEX]; //用於存儲到各點最短路徑的權值和

//Dijkstra算法,求有向網G的頂點V0頂點到其餘頂點v最短路徑P[v]及帶權長度D[v]
//P[v]的值爲前驅頂點下標,D[v]表示v0到v的最短路徑長度和
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *p, ShortPathTable *D)
{
    int v, w, k, min;
    int final[MAXVEX];          //final[w] = 1 表示求得頂點v0至vw的最短路徑
    for (v = 0; v < G.numVertexes; ++v)     //初始化數據
    {
        final[v] = 0;       //全部頂點初始化爲未知最短路徑狀態
        (*D)[v] = G.arc[v0][v];     //將與v0點有連線的頂點加上權值
        (*p)[v] = 0;                //初始化路徑數組P爲0
    }
    (*D)[v0] = 0;               //v0至v0路徑爲0
    final[v0] = 1;              //v0至v0不需要求路徑
    //開始主循環,每次求得v0到某個v頂點的最短路徑
    for (v = 1; v < G.numVertexes; ++v)
    {
        min = INFINITY;             //當前所知離v0頂點的最近距離
        for (w = 0; w < G.numVertexes; ++w)     //尋找離v0最近的頂點
        {
            if (!final[w] && (*D)[w] < min)
            {
                k = w;
                min = (*D)[w];      //w頂點離v0頂點更近
            }
        }
        final[k] = 1;           //將目前找到的最近的頂點置爲1
        for (w = 0; w < G.numVertexes; ++w)     //修正當前最短路徑及距離
        {
            //如果經過v頂點的路徑比現在這條路徑的長度短的話
            if (!final[w] && (min + G.arc[k][w] < (*D)[w]))
            {   //說明找到了更短的路徑,修改D[w]和p[w]
                (*D)[w] = min + G.arc[k][w];    //修改當前路徑長度
                (*P)[w] = k;
            }
        }
    }
}

通過迪傑斯特拉算法解決了從某個源點到其餘各頂點的最短路徑問題。算法時間複雜度爲O(n2 ).若是只找到從源點到某一個特定終點的最短距離,時間複雜度依然是O(n2 )。若是求任一頂點到其餘所有頂點的最短路徑,此時整個算法時間複雜度爲O(n3 )。

7.7.2 弗洛伊德(Floyd)算法

它求所有頂點到所有頂點的時間複雜度是O(n3 )。但是算法非常簡潔優雅。代碼如下,注意是求所有頂點到所有頂點的最短路徑,因此,Pathmatirx和ShortPathTable都是二維數組

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];

//floyd算法,求網圖G中各頂點v到其餘頂點w最短路徑P[v][w]及帶權長度D[v][w]
void ShortestPath_Floyd(MGraph G, Pathmatirx *p, ShortPathTable *D)
{
    int v,w,k;
    for (v = 0; v < G.numVertexes; ++v)     //初始化D與P
    {
        for (w = 0; w < G.numVertexes; ++w)
        {
            (*D)[v][w] = G.matirx[v][w];    //D[v][w]值即爲對應點間的權值
            (*P)[v][w] = w;                 //初始化P
        }
    }
    for (k = 0; k < G.numVertexes; ++k)
    {
        for (v = 0; v < G.numVertexes; ++v)
        {
            for (w = 0; w < G.numVertexes; ++w)
            {
                if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
                {//如果經過下標爲k頂點路徑比原兩點間路徑更短,將當前兩點間權值設爲更小的一個
                    (*D)[v][w] = (*D)[v][k] + (*D)[k][w];
                    (*P)[v][w] = (*P)[v][k]; //路徑設置經過下標爲k的頂點
                }
            }
        }
    }           
}

//求最短路徑的顯示代碼
for (v = 0; v < G.numVertexes; ++v)
{
    for (w = v+1; w < G.numVertexes; ++w)
    {
        printf("v%d-v%d-v 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\n", w);      //打印終點
    }
    printf("\n");
}

7.8 拓撲排序

7.8.1 拓撲排序的介紹

在一個表示工程的有向圖中,用頂點表示活動,弧表示活動之間的優先關係,這樣的有向圖爲頂點表示活動的網,稱爲AOV網(Activity On Vertex Network)。
  設G=(V,{E})是一個具有n個頂點的有向圖,V中的頂點序列v1,v2,.......,vn 滿足若從vivj 有一條路徑,則在頂點序列中頂點vi 必須在vj 之前。則稱這樣的頂點序列爲一個拓撲序列。
  所謂拓撲排序,其實就是對一個有向圖構造拓撲序列的過程。

7.8.2 拓撲排序算法

  對AOV進行拓撲排序的基本思路是:從AOV網中選擇一個入度爲0的頂點輸出,然後刪去此頂點,並刪除以此頂點爲尾的弧,繼續重複此步驟,直到輸出全部頂點或者AOV網中不存在入度爲0的頂點爲止。
  確定一下這個圖需要使用的數據結構。爲AOV網建立一個鄰接表,考慮到算法過程中要查找入度爲0的頂點,在原來頂點表結點結構中,增加一個入度域in,結構如圖所示,其中in就是入度的數字。
  這裏寫圖片描述
  因此對於圖中的第一幅圖AOV網,可以得到如第二幅圖的鄰接表數據結構。
  這裏寫圖片描述
  這裏寫圖片描述
  在拓撲排序算法中,涉及的結構代碼如下:

typedef struct EdgeNode     //邊表結點
{
    int adjvex;         //鄰接點域,存儲該頂點對應的下標
    int weight;         //用於存儲權值,對於非網圖可以不需要
    struct EdgeNode *next;  //鏈域,指向下一個鄰接點
}EdgeNode;

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

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

在算法中,還需要輔助的數據——棧,用來存儲處理過程中入度爲0的頂點,目的是爲了避免每個查找時都要去遍歷頂點表找有沒有入度爲0的頂點。

//拓撲排序,若GL無迴路,則輸出拓撲排序序列並返回OK,若有迴路返回ERROR
Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i, k, gettop;
    int top = 0;        //用於棧指針下標
    int count = 0;      //用於統計輸出頂點的個數
    int *stack;         //建棧存儲入度爲0的頂點
    stack = (int *) malloc(GL->numVertexes * sizeof(int));
    for (i = 0; i < GL->numVertexes; ++i)
        if (GL->adjList[i].in == 0)
            stack[++top] = i;       //將入度爲0的頂點入棧
    while (top != 0)
    {
        gettop = stack[top--];      //出棧
        printf("%d->",GL->adjList[gettop].data);    //打印此頂點
        count++;
        for (e = GL->adjList[gettop].firstedge; e; e = e->next)
        {//對此頂點弧表遍歷
            k = e->adjvex;
            if (!(--GL->adjList[k].in)) //將k號頂點鄰接點的入度減1
                stack[++top] = k;       //若爲0則入棧,以便於下次循環輸出
        }
    }
    if (count < GL->numVertexes)    //如果count小於頂點數,說明不存在環
        return ERROR;
    else 
        return OK;
}

分析整個算法,對一個具有n個頂點e條弧的AOV網來說,整個算法的時間複雜度爲O(n+e)。

7.9 關鍵路徑

  拓撲排序主要是爲解決一個工程能否順序進行的問題,但有時還需要解決工程完成需要的最短時間問題。
  在一個表示工程的帶權有向圖中,用頂點表示事件,用有向邊表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,稱之爲AOE網(Activity On Edge Network)。把AOE中沒有入邊的頂點稱爲始點或源點,沒有出邊的頂點稱爲終點或匯點。正常情況下,AOE網只有一個始點或匯點。
  儘管AOE網與AOV網都是用來工程建模的,但它們有很大的不同,主要體現在AOV網是頂點表示活動的網,它只描述活動之間的制約關係,而AOE網是用邊表示活動的網,邊上的權值表示活動持續的時間。AOE網是要建立在活動之間制約關係沒有矛盾的基礎上,再來分析完成整個工程至少需要多少時間,或者爲縮短完成工程所需的時間,應當加快那些活動等問題。
  路徑上各個活動所持續的時間之和稱爲路徑長度,從源點到匯點具有最大長度的路徑叫關鍵路徑,在關鍵路徑上的活動叫關鍵活動

7.9.1 關鍵路徑算法原理

只需要找到所有活動的最早開始時間和最晚開始時間,並且比較它們,如果相等就意味着此活動是關鍵活動,活動間的路徑爲關鍵路徑。如果不等,則就不是。
爲此,需要定義幾個參數:

  1. 事件的最早發生時間etv(earliest time of vertex):即頂點vk 的最早發生時間。
  2. 事件的最晚發生時間ltv(latest time of vertex):即頂點vk 的最晚發生時間,也就是每個頂點對應的事件最晚需要開始的時間,超出此時間將會延遲整個工期。
  3. 活動的最早開工時間ete(earliest time of edge):即弧ak 的最早發生時間。
  4. 活動的最晚開工時間lte(latest time of edge):即弧ak 的最晚發生時間,也就是不推遲工期的最晚開工時間。
      由1和2可以求得3和4,然後再根據ete【k】是否與lte【k】相等來判斷ak 是否是關鍵活動。

7.9.2 關鍵路徑算法

  將AOE網轉換爲鄰接表結構,注意與拓撲排序時鄰接表結構不同的地方在於,這裏的弧鏈表增加了weight域,用來存儲弧的權值。

  求事件的最早發生時間etv的過程,就是從頭至尾找拓撲序列的過程,因此:在求關鍵路徑之前,需要先調用一次拓撲序列算法的代碼來計算etv和拓撲序列列表。爲此,首先在程序開始處聲明幾個全局變量
這裏寫圖片描述

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

其中stack2用來存儲拓撲序列,以便後面求關鍵。
下面是改進過的求拓撲序列算法

//拓撲排序 ,用於關鍵路徑計算
Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i, k, gettop;
    int top = 0;        //用於棧指針下標
    int count = 0;      //用於統計輸出頂點的個數
    int *stack;         //建棧將入度爲0的頂點入棧
    stack = (int *) malloc(GL->numVertexes * sizeof(int));
    for (i = 0; i < GL->numVertexes; ++i)
        if ( 0 == GL->adjList[i].in)
            stack[++top] = i;
    top2 = 0;       //初始化爲0
    etv = (int *)malloc(GL->numVertexes * sizeof(int)); //事件最早發生時間
    for (i = 0; i < GL->numVertexes; ++i)
        etv[i] = 0;     //初始化爲0
    stack2 = (int *)malloc(GL->numVertexes * sizeof(int));  //初始化
    while (top != 0)
    {
        gettop = stack[top--];
        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;
            if ((etv[gettop]+e->weight)->etv[k])    //求各頂點事件最早發生時間值
                etv[k] = etv[gettop] + e->weight;
        }
    }
    if (count < GL->numVertexes)
        return ERROR;
    else 
        return OK;
}

其中P[K]表示所有到達頂點vk 的弧的集合。下面來看求關鍵路徑的算法代碼:

//求關鍵路徑,GL爲有向網,輸出GL的各項關鍵活動
void CriticalPath(GraphAdjList GL)
{
    EdgeNode *e;
    int i, gettop, k, j;
    int ete, lte;       //聲明活動最早發生時間和最遲發生時間變量
    TopologicalSort(GL);    //求拓撲序列,計算數組etv和stack2的值
    ltv = (int *)malloc(GL->numVertexes * sizeof(int)); //事件最晚發生時間
    for (i = 0; i < GL->numVertexes; ++i)
        ltv[i] = etv[GL->numVertexes-1];    //初始化ltv
    while (top2 != 0)       //計算ltv
    {
        gettop = stack2[top--];     //將拓撲序列出棧,後進先出
        for (e = GL->adjList[gettop].firstedge; e; e = e->next)
        {//求各頂點事件的最遲發生時間ltv值
            k = e->adjvex;
            if (ltv[k] - e->weight < ltv[gettop])   //求各頂點事件最晚發生時間ltv
                ltv[gettop] = ltv[k] - e->weight;
        }
    }
    for (j = 0; j < GL->numVertexes; ++j)       //求ete,lte和關鍵活動
    {
        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("<v%d,v%d> length: %d, ", GL->adjList[j].data, GL->adjList[k].data, e->weight);
        }
    }
}

分析整個算法,最終求關鍵路徑算法的時間複雜度依然是O(n+e)。

7.10 總結回顧

7.11 結尾語

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