《大話數據結構》第七章 圖


第七章 圖

圖的定義

定義:圖是由頂點的有窮非空集合頂點之間邊的集合組成,通常表示爲:G(V, E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。

注意

  1. 圖中的數據元素,稱爲頂點(Vertex)
  2. 圖結構中不允許沒有頂點
  3. 任意兩個頂點之間都可能有關係,頂點之間的邏輯關係用邊表示

無向邊:兩個頂點之間的邊沒有方向,就稱這邊爲無向邊(Edge),用無序偶對(vi,vj)(v_i,v_j)表示。
如果所有的頂點之間都是無向邊,則這個圖爲無向圖(Undirected graphs)。

有向邊:頂點間的邊有方向,則稱這條邊爲有向邊,也成爲弧(Arc)。
如果所有的邊都是有向邊,則該圖稱爲有向圖(Directed graphs)。
由A到D的有向邊,A是弧尾,D是弧頭,<A, D>表示弧,不可寫成<D, A>

注意:無向邊是小括號(),有向邊是尖括號 <>

簡單圖:若不存在頂點到其自身的邊,且同一條不重複出現,則稱這樣的圖爲簡單圖,下面這兩個都不是:

無向完全圖:如果無向圖中任意兩個頂點之間都存在邊,則稱該圖爲無向完全圖。
n個頂點的無向完全圖有n×(n1)2\frac{n\times (n-1)}{2}條邊。

有向完全圖:如果有向圖中任意兩個頂點之間都存在方向互爲相反的兩條弧,則稱該圖爲有向完全圖。
n個頂點的有向完全圖有n×(n1)n\times (n-1)條邊。

稠密圖:很多條邊或弧的圖。
稀疏圖:很少條邊或弧的圖。

:有些圖的邊或弧具有相關數字,這種與圖的邊或弧相關的數叫做權(Weight)。

:這些權可以表示點之間的距離或耗費,帶權的圖通常稱爲網(Network)。

子圖:A被B包含,則A是B的子圖,下面的右邊都是左邊的子圖。


圖的頂點和邊間關係

無向的鄰接和依附:如果兩個點(v,v)(v,v')之間有邊,則稱這兩個頂點互爲鄰接點(Adjacent),即v,vv,v'相鄰接。邊依附於頂點v,vv,v',或者說邊和頂點相關聯。

頂點的度:和該頂點相關聯的邊的數目。
邊的數量就是各頂點度數和的一半。

有向的鄰接:如果有弧<v,v>< v,v' >存在,則稱頂點vv鄰接到頂點vv',頂點vv'鄰接自頂點vv(以頭爲主)。弧和頂點相關聯。

入度:以頂點v爲頭的弧的數目稱爲v的入度(InDegree),記爲ID(v)。

出度:以v爲結尾的弧的數目稱爲v的出度(OutDegree),記爲OD(v)。

:頂點的度 TD = ID + OD

無向圖的路徑:從頂點vvvv'的路徑是一個頂點序列(v=vi,0,vi,1,...,vi,m=v)(v=v_{i,0},v_{i,1},...,v_{i,m} = v'),其中(vi,j1,vi,jE,1jm)(v_{i,j-1},v_{i,j} \in E, 1 \le j \le m)

B到D四種路徑:

有向圖的路徑:路徑也是有向的,B到D只有兩種路徑

有向圖中根結點到任意結點的路徑是唯一的。

路徑的長度:路徑上的邊或弧的數目。

迴路/環:第一個頂點到最後一個頂點相同的路徑。

簡單路徑:序列中頂點不重複出現的路徑。

簡單迴路/環:除了第一個頂點和最後一個頂點之外,其餘頂點不重複出現的迴路,稱爲簡單迴路或簡單環。

途中的粗線都是環,左側是簡單環,右側因爲C頂點重複出現所以不是簡單環。


連通圖相關術語

連通圖:無向圖中,如果頂點之間有路徑,那麼稱這兩個頂點是連通的。如果對於圖中的任意頂點都是連通的,則稱該圖爲連通圖(Connected Graph)。

左邊不是連通圖,右邊是:

連通子圖:無向圖中的極大連通子圖成爲連通分量,要注意:

  1. 要是子圖
  2. 子圖要是連通的
  3. 連通子圖含有極大頂點數
  4. 具有極大頂點數的連通子圖包含依附於這些頂點的所有邊

下面圖2和圖3是圖1的連通分量,圖4雖然是圖1的子圖,但是不滿足連通子圖的極大的頂點數

非連通圖可以有連通分量

極大連通子圖:包含了原圖和子圖頂點關聯的所有邊
極小連通子圖:包含子圖連通必不可少的邊。

強連通圖:有向圖中,如果每一對頂點間都存在路徑,則稱G十強連通圖。

強連通分量:有向圖中的極大強連通子圖

下面圖1不是強連通圖,圖2是圖1的極大連通子圖

連通圖的生成樹:一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的n各頂點,但只有足以構成一棵樹的n-1條邊。


圖1不是,因爲邊太多了,還可以砍掉一些;
圖2和圖3是;
圖4雖然是n-1條邊,但是不連通。

有向樹:如果一個有向圖恰有一個頂點的入度爲0,其餘頂點的入度均爲1,則是一棵有向樹。

有向圖的生成森林:由若干棵有向數組成,含有圖中全部頂點,但是隻有構成有向樹不相交必須個數的弧。

圖1是有向圖,去掉一些弧後,分解爲兩棵有向樹,右邊的兩棵有向樹就是圖1有向圖的生成森林。


圖的存儲結構

圖的抽象數據類型


鄰接矩陣

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

nxn,右邊就設爲1,沒有就爲0,這樣不也是浪費很多空間嗎

無向圖

無向圖的邊數組是一個對稱矩陣
優點:容易判定兩點間有無邊;容易計算度;

有向圖

有向圖的矩陣並不對稱。
頂點v1v_1的入度是第v1v_1列各數之和,出度是第v1v_1行個數之和。

有權圖

鄰接矩陣存儲結構

typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535    // 用65535表示無窮

typedef struct MGraph
{
    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->numEdges; j++)
            G->arc[i][j] = INFINITY;
    for (k=0; k < G->numEdges; k++)    // 建立鄰接矩陣
    {
        printf("輸入前後下標i,j和權重w");
        scanf("%d, %d, %d", &i, &j, &w);
        G->arc[i][j] = w;
        G->arc[j][i] = G->arc[i][j];    // 無向圖,矩陣對稱
    }
}

時間複雜度
創建的複雜度:O[n+n2+e]O[n+n^2+e]
初始化鄰接矩陣G->arc的複雜度:O[n2]O[n^2]


鄰接表

當邊數相對於頂點數較少的時候,鄰接矩陣很浪費存儲空間,如下面的例子:

結構:數組和鏈表相結合的存儲方法稱爲鄰接表。

處理方法

  1. 頂點用一個一維數組存儲,每個數據元素還要存儲指向第一個鄰接點的指針,以便於查找該頂點的邊信息
  2. 每個頂點的所有鄰接點構成一個線性表,由於鄰接點的個數不定,所以用單鏈表存儲,無向圖稱爲頂點v1v_1的邊表,有向圖則稱爲頂點v1v_1作爲弧尾的出邊表

無向圖

adjvex存放鄰接點域,爲結點下標。

有向圖

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

逆的就是左邊爲弧尾,右邊爲弧頭

某個頂點的入度或出度可以通過鏈表長度判斷

對於帶權值的,還可以再加個數據域

結構定義

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 GraphAdjList
{
    AdjList adjList;
    int numVertexes, numEdges;
} GraphAdjList;

無向圖的鄰接表

void CreateALGraph(GraphAdjList *G)
{
    int i, j, k;
    EdgeNode *e;
    printf("輸入頂點數和邊數");
    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("輸入邊上的頂點序號");
        scanf("%d, %d", &i, &j);
        // 給i後邊的鏈表添加元素j,使用頭插法
        e = (EdgeNode *) malloc(sizeof(EdgeNode));    // 生成邊表結點
        e->adjvex = j;    // 鄰接序號爲j
        e->next = G->adjList[i].firstegde;    // e指針指向當前頂點指向的結點
        G->adjList[i].firstedge = e;    // 將當前頂點指針指向e
        // 因爲是無向所以是對稱的,也要個j後面加i
        e = (EdgeNode *) malloc(sizeof(EdgeNode));
        e->adjvex = i;
        e->next = G->adjList[j].firstedge;
        G->adjList[j].firstegde = e;
    }
}

代碼說明

  • 先建立頂點表結點,同時輸入數據,把指向結點設爲NULL。
  • 邊錶鏈表中的每個元素之間是沒有關聯的,每個元素是指向這個鏈表的頂點的鄰接點,所以如果有頂點的鄰接點就用頭插法插到鏈表中。
  • 因爲是無向圖,所以是對稱的,要做兩遍插入。
  • 時間複雜度爲O[n+e]O[n+e]

十字鏈表

思想:把鄰接表和逆鄰接表結合起來。

頂點表結點結構

firstin表示入邊表頭指針,指向該頂點的入邊表中第一個結點;
firstout表示出邊表頭指針,指向出邊表中的第一個結點。

邊表結點結構

talvex是指弧起點在頂點表的下標;
headvex是指弧終點在頂點表中的下標;
headlink是指入邊表的指針域,指向終點相同的下一條邊;
taillink是指邊表指針域,指向起點相同的下一條邊;
網可以再加個weight域。

加入v0指向v1,然後在v0的指向v1的表的taillink中存放另一個指向v1的頂點?

核心理解

  • 通過firstin再加headlink可以找到該頂點的所有入邊
  • 通過firstort再加taillink可以找到該頂點的所有出邊

實現思路
比如輸入<v1,v0>< v_1, v_0 >,此時先建立v1指向v0,新建一個邊表結點,讓v1的fisrtout指向新邊表結點,給邊表結點的tailvex和headvex賦值,同時取b0的fisrtin指向,如果爲空就賦值給firstin,如果不爲空就一直鄉下找headlink,直到找到爲空的headlink,將新邊表結點賦值給空的headlink。

優點

  • 求以某頂點爲頭/尾的弧很方便,也很容易得到出度和入度。
  • 時間複雜度和鄰接表相同。

鄰接多重表

如果要對邊進行操作,那麼鄰接表就不方便了,如下圖,如果要去掉(v0,v2)(v_0, v_2)這條邊,對右邊的表的操作比較繁瑣。

邊表結點結構

其中ivex和jvex是和某邊相關的兩個頂點在頂點表中的下標。
ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊。


首先要記住幾點

  • ilink會指向viv_i頂點的另一條邊,而在另一條邊中,viv_i頂點肯定就不是ii了,此時是新邊的jj,所以ilink指向的結點的jj就是原來的ii
  • 如果有nn個結點,那麼右圖就會有2n2n條連線。

流程
跟上邊的鄰接表差不多,之前的鄰接表是一個頂點連接結點的鏈表,而鄰接多重是採用指向的方法,一個頂點後面只有一個結點,通過結點來指向,這樣對邊操作就很方便,如果要刪除(v0,v2)(v_0,v_2),只需要將6,9的鏈接設爲^即可。


邊集數組

結構:邊集數組是由兩個一維數組組成,一個存儲頂點信息;另一個存貯邊的信息,這個邊數組的每個數據元素由:一條邊的起點下標(begin)、終點下標(end)、權(weight)組成。

這個是最好理解的了,直接暴力存儲

邊數組結構


JAVA實現鄰接表/矩陣的創建

出錯的點:

  • 見了一個類的數組,但是沒有對每個數組元素做類的初始化
  • 輸入的下標和頂點表中不匹配,所以多加了個位置
package Graph;

import java.util.Scanner;

class VertexList{
    public char data;
    public adjNode firstNode;
    public int weight;

    public VertexList(char data, int weight) {
        this.data = data;
        this.weight = weight;
        firstNode = null;
    }
}

class adjNode{
    public int data;
    public adjNode next;

    public adjNode() {
        next = null;
    }
}

public class Base {
    public static void main(String[] args) {
        int[][] adjMat  = CreAdjMat();
        VertexList[] vertexLists = CreaAdjList();

    }

    private static VertexList[] CreaAdjList() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("輸入頂點個數和邊數:");
        int numVer = scanner.nextInt();
        int numEdge = scanner.nextInt();
        VertexList[] vertexLists = new VertexList[numVer+1];
        System.out.println("num ver" + numVer);
        // 初始化頂點表
        for (int i = 1; i < numVer+1; i++) {
            System.out.println("輸入頂點:");
            char data = scanner.next().charAt(0);
            System.out.println("輸入頂點權重:");
            int weight = scanner.nextInt();
            // 忘了初始化了,要先初始化才能賦值
            vertexLists[i] = new VertexList(data, weight);
        }
        // 輸入邊
        // 注意下標,這裏我把長度都+1了
        for (int i = 0; i < numEdge; i++) {
            System.out.println("輸入邊的頂點序號:");
            int s = scanner.nextInt();
            int e = scanner.nextInt();
            // 兩個頂點都接入邊,先從s開始
            adjNode node1 = new adjNode();
            node1.data = e;
            // 用頭插法,原本的放在新的後面
            node1.next = vertexLists[s].firstNode;
            // 新的插在頂點後
            vertexLists[s].firstNode = node1;

            // 同樣的方法再處理e
            adjNode node2 = new adjNode();
            node2.data = s;
            node2.next = vertexLists[e].firstNode;
            vertexLists[e].firstNode = node2;
        }

        return vertexLists;

    }

    private static int[][] CreAdjMat() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("輸入的頂點數和邊數");
        int numVer = scanner.nextInt();
        int numEdge = scanner.nextInt();
        int[] vertexes = new int[numVer];
        int[][] edge = new int[numVer][numVer];
        for (int i = 0; i < numVer; i++) {
            for (int i1 = 0; i1 < numVer; i1++) {
                edge[i][i1] = 0;
            }
        }
        for (int i = 0; i < numEdge; i++) {
            System.out.println("輸入邊:");
            int v1 = scanner.nextInt();
            int v2 = scanner.nextInt();
            edge[v1][v2] = 1;
        }
        return edge;
    }
}

圖的遍歷

定義:從圖中某一頂點出發遍歷圖中其餘頂點,且使每一個頂點僅被訪問一次,這一過程就叫圖的遍歷(Traversing Graph)。
通常有兩種遍歷方案:一種是深度優先遍歷,一種是廣度優先遍歷。

深度優先遍歷

DFS:深度優先遍歷(Depth First Search),也有稱爲深度優先搜索,簡稱DFS。
類似找鑰匙的時候一個屋一個屋地搜,每個屋徹底搜完再搜下一個,類似於樹的前序遍歷


右圖過程
從圖中的某個頂點v出發,訪問此頂點,然後從v的未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和v有路徑相同的頂點都被訪問到。
對於非連通圖,如果經過一次深度優先遍歷後仍有頂點沒有被訪問,則另選途中一個未被訪問的頂點作初始點,重複上述過程,直至圖中所有頂點都被訪問到爲止。

思路
深度優先就是一條道走到黑,抓到一個輸出一個,所以用遞歸的方法來在一個點的鄰接域中做文章,然後鄰接域中的點再作爲頂點,如此遞歸。每個DFS中標記已經見過。

鄰接矩陣遍歷代碼
鄰接矩陣就是n×nn\times n標記0或1那個

typedef int Boolean;
Boolean visited[MAX];    // 訪問標誌的數組


// 鄰接矩陣的深度優先遞歸算法
// 打印所有和i點相鄰接的沒訪問過頂點
void DFS(MGraph G, int i)
{
    int j;
    visited[i] = TRUE;
    printf("%c", G.vexs[i]);    // 打印頂點
    for (j=0; j<G.numVertexes; j++)
        // 和i相鄰接 且 沒訪問過
        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])    // 對未訪問過的idan調用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)
    {
        // 思路差不多,順着鏈表一直往下找,同時每次找到的新頂點都做DFS
        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(GL, i);
}

兩種結構遍歷的時間複雜度

  • 鄰接矩陣需要訪問矩陣中的所有元素,因此爲O[n2]O[n^2]
  • 鄰接表需要的時間取決於頂點 n 和邊 e 的數量,爲O[n+e]O[n+e]

對於點多邊少的稀疏圖來說,鄰接表的效率更高。


廣度優先遍歷

BFS:廣度優先遍歷(Breadth First Search),又稱爲廣度優先搜索,簡稱BFS。
類似找鑰匙先把每個房間大概看一遍,慢慢擴大範圍,類似於樹的層序遍歷

重構了一下左邊的圖,變成了右邊,邊和頂點的關係是不變的:
選擇A爲第一層;選擇A的鄰接點BF爲第二層;選擇BF的鄰接點CIGE爲第三層;選擇CIGE的鄰接點DH爲第四層。


思路
廣度優先就是平向來做,一層一層的處理,通過同一頂點的鄰接點都入棧來完成,而不是從一個鄰接點一直深入。入棧的時候纔可標記爲已經見過。

鄰接矩陣遍歷代碼

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", GL->adjList[i].data);
            EnQueue(&Q, i);
            while (!QueueEmpty(Q))
            {
                DeQueue(&Q, &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;
                }
            }
        }
    }
}

JAVA實現深度/廣度遍歷

package Graph;

import java.util.ArrayList;
import java.util.List;

class Queue{
    private int top;
    private int max;
    private int[] list;

    public Queue(int max) {
        this.top = 0;
        this.list = new int[max];
    }

    public void push(int idx){
        list[top++] = idx;
    }

    public int pop(){
        top--;
        return list[top];
    }

    public int Empty(){
        if (top==0){
            return 1;
        }
        else {
            return 0;
        }
    }
}

public class Travers {
    public static void main(String[] args) {
        // 手動鄰接矩陣
        // 手動鄰接表
        VertexList[] vertexList = new VertexList[10];
        int[][] edgeList = new int[16][2];
        // 初始化
        int[][] edgeMat = new int[10][10];
        InitVerAjd(vertexList, edgeList);
        // 創建鄰接表
        CreateAdjList(vertexList, edgeList);
        // 測試創建是否正確
        // System.out.println(vertexList[3].data);
        // System.out.println(vertexList[3].firstNode.data);
        // System.out.println(vertexList[3].firstNode.next.data);
        // System.out.println(vertexList[3].firstNode.next.next.data);

        // 創建鄰接矩陣
        CreateAdjMat(edgeList, edgeMat);

        // 鄰接矩陣的深度遍歷
        System.out.println("鄰接矩陣的深度遍歷------------");
        TraverMatDFS(vertexList, edgeMat);

        // 鄰接表的深度遍歷
        System.out.println("鄰接表的深度遍歷------------");
        TraverListDFS(vertexList, edgeList);

        // 鄰接矩陣的廣度遍歷
        System.out.println("鄰接矩陣的廣度遍歷------------");
        TraverMatBFS(vertexList, edgeMat);

        // 鄰接表的廣度遍歷
        System.out.println("鄰接表的廣度遍歷------------");
        TraverListBFS(vertexList, edgeList);

    }

    private static void TraverListBFS(VertexList[] vertexList, int[][] edgeList) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 1; i < len; i++) {
            visit[i] = 0;
        }
        Queue que = new Queue(len-1);
        for (int i = 1; i < len; i++) {
            if (visit[i] == 0){
                que.push(i);
                visit[i] = 1;
                System.out.println(vertexList[i].data);
                while (que.Empty() == 1){
                    int idx = que.pop();
                    adjNode q = vertexList[idx].firstNode;

                    // 順着鄰接表做廣度
                    while (q != null){
                        if (visit[q.data]==0){
                            // 鄰接入棧
                            que.push(q.data);
                            visit[q.data] = 1;
                            System.out.println(vertexList[q.data].data);
                        }
                        q = q.next;
                    }

                }

            }
        }

    }

    private static void TraverMatBFS(VertexList[] vertexList, int[][] edgeMat) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 1; i < len; i++) {
            visit[i] = 0;
        }
        Queue que = new Queue(len-1);
        for (int i = 1; i < len; i++) {
            if (visit[i] == 0){
                que.push(i);
                visit[i] = 1;
                System.out.println(vertexList[i].data);
                // 棧裏有就一直找
                while (que.Empty() == 0){
                    int idx = que.pop();
                    // 並不是從這裏標記是否看見,入棧的時候才能標記
                    // 從i的鄰接點下手,沒見過的都入棧

                    for (int i1 = 1; i1 < len; i1++) {
                        // 沒見過且有鄰接
                        if (visit[i1]==0 && edgeMat[idx][i1]==1){
                            // 打印且入棧
                            System.out.println(vertexList[i1].data);
                            que.push(i1);
                            visit[i1] = 1;
                        }
                    }
                }
            }
        }
    }

    private static void TraverListDFS(VertexList[] vertexList, int[][] edgeList) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 1; i < len; i++) {
            visit[i] = 0;
        }
        for (int i = 1; i < len; i++) {
            if (visit[i]==0){
                DFSList(vertexList, visit, i);
            }
        }
    }

    private static void DFSList(VertexList[] vertexList, int[] visit, int idx) {
        visit[idx] = 1;
        System.out.println(vertexList[idx].data);
        // 往後找
        adjNode p = vertexList[idx].firstNode;
        while (p != null){
            if (visit[p.data]==0){
                DFSList(vertexList, visit, p.data);
            }
            p = p.next;
        }

    }

    // 深度優先鄰接矩陣用
    private static void DFSMat(VertexList[] vertexList, int[][] edgeMat, int[] visit, int idx){
        int len = edgeMat.length;
        // 這兩個在循環的外面,每次調用DFS會打印一個
        // 打印和控制都在循環外面進行
        visit[idx] = 1;
        System.out.println(vertexList[idx].data);

        for (int i = 1; i < len; i++) {
            if(edgeMat[idx][i]==1 && visit[i]==0){
                DFSMat(vertexList, edgeMat, visit, i);
            }
        }
    }

    // 深度優先鄰接矩陣遍歷
    private static void TraverMatDFS(VertexList[] vertexList, int[][] edgeMat) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 0; i < len; i++) {
            visit[i] = 0;
        }
        for (int i = 1; i < len; i++) {
            if(visit[i]==0){
                DFSMat(vertexList, edgeMat, visit, i);
            }

        }
    }

    private static void CreateAdjMat(int[][] edgeList, int[][] edgeMat) {
        int len0 = edgeMat.length;
        for (int i = 0; i < len0; i++) {
            for (int i1 = 0; i1 < len0; i1++) {
                edgeMat[i][i1] = 0;
            }
        }
        int len = edgeList.length;
        for (int i = 0; i < len; i++) {
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            edgeMat[s][e] = 1;
            edgeMat[e][s] = 1;
        }
    }


    private static void CreateAdjList(VertexList[] vertexList, int[][] edgeList) {
        int len = edgeList.length;
        for (int i = 1; i < len; i++) {
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            adjNode node1 = new adjNode();
            node1.data = s;

            node1.next = vertexList[e].firstNode;
            vertexList[e].firstNode = node1;

            adjNode node2 = new adjNode();
            node2.data = e;
            node2.next = vertexList[s].firstNode;
            vertexList[s].firstNode = node2;
        }
    }

    private static void InitVerAjd(VertexList[] vertexList, int[][] edgeList) {
        vertexList[1] = new VertexList('A',0);
        vertexList[2] = new VertexList('B',0);
        vertexList[3] = new VertexList('C',0);
        vertexList[4] = new VertexList('D',0);
        vertexList[5] = new VertexList('E',0);
        vertexList[6] = new VertexList('F',0);
        vertexList[7] = new VertexList('G',0);
        vertexList[8] = new VertexList('H',0);
        vertexList[9] = new VertexList('I',0);
        // 1
        edgeList[1][0] = 1;
        edgeList[1][1] = 2;
        // 2
        edgeList[2][0] = 2;
        edgeList[2][1] = 3;
        // 3
        edgeList[3][0] = 3;
        edgeList[3][1] = 4;
        // 4
        edgeList[4][0] = 4;
        edgeList[4][1] = 5;
        // 5
        edgeList[5][0] = 5;
        edgeList[5][1] = 6;
        // 6
        edgeList[6][0] = 6;
        edgeList[6][1] = 1;
        // 7
        edgeList[7][0] = 2;
        edgeList[7][1] = 7;
        // 8
        edgeList[8][0] = 7;
        edgeList[8][1] = 6;
        // 9
        edgeList[9][0] = 2;
        edgeList[9][1] = 9;
        // 10
        edgeList[10][0] = 3;
        edgeList[10][1] = 9;
        // 11
        edgeList[11][0] = 9;
        edgeList[11][1] = 4;
        // 12
        edgeList[12][0] = 4;
        edgeList[12][1] = 8;
        // 13
        edgeList[13][0] = 7;
        edgeList[13][1] = 4;
        // 14
        edgeList[14][0] = 8;
        edgeList[14][1] = 5;
        // 15
        edgeList[15][0] = 7;
        edgeList[15][1] = 8;
    }
}

最小生成樹

定義:把構造連通網的最小代價生成樹稱爲最小生成樹(Minimum Cost Spanning Tree)。

Prim算法

lowcost數組的作用
長度爲頂點個數的一維數組,初始化下標爲0的位置的值爲0,其他全爲無窮大,下標指向的內容爲0時,說明下標代表的頂點已經在最小生成樹中了。
這個數組用來存放所有未在最小生成樹中的頂點中,距離已經在最小生成樹中的頂點的最小距離,簡單來說就是未在最小生成樹中的頂點和最小生成樹的最小距離。最小生成樹中尚未有鄰接點的頂點的值,設置爲無窮大。

adjvex數組的作用
lowcost中存放的是頂點和最小生成樹的最小距離,adjvex數組來記錄離最小生成樹最近的點最小生成樹中哪個點距離最近,數組離存放對應的最小生成樹的點,以此來生成邊。

算法流程

  1. 創建一個lowcost和一個adjvex數組,長度都爲頂點數,lowcost全部初始化爲無窮,然後第一個值設爲0,表示v0v_0結點已經在最小生成樹中;adjvex數組全部初始化爲0;
  2. 對每個頂點進行遍歷,每次找lowcost中的權值最小的位置,然後用adjvex來知道是距離最小生成樹中哪個點最近,這時就能生成一個邊(adjvex[k], k);
  3. 設lowcost[k]=0,就是將該點納入最小生成樹中;
  4. 此時再循環所有的頂點,如果有離最小生成樹距離更近的,就更新adjvex的值,記錄對應位置。

代碼實現

void MiniSpanTree_Prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];    // 保存相關下標
    int lowcost[MAXVEX];    // 保存權值
    // 初始化
    lowcost[0] = 0;
    adjvex[0] = 0;
    for (i=1; i<G.numVertexes; i++)
    {
        lowcost[i] = G.arc[0][i];
        adjvex[i] = 0;
    }
    for (i=1; i<G.numVertexes; i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j < G.numVertexes)    // 循環全部的點
        {
            // 找權值最小的點
            if (lowcost[j]!=0 && lowcost[j]<min)
            {
                min = lowcost[j];
                k = j;
            }
            j++;
        }
        // k離最小生成樹中最近的點是adjvex[k]
        printf("(%d, %d)", adjvex[k], k);    // 打印當前頂點邊中權值最小邊
        lowcost[k] = 0;    // 當前頂點的權值設置爲0, 表示此頂點已經完成任務
        // 新點進去,檢測是否會讓剩下的點離最小生成樹更近
        for (j=1; j<G.numVertexes; j++)
        {
            // 尋找剛找出的k的鄰接點中,離最小生成樹更近的點
            if (lowcost[j]!=0 && G.arc[k][j] <lowcost[j])
            {
                // 若下標爲k頂點某鄰接邊權值小於此前選中的點,就更新
                lowcost[j] = G.arc[k][j];    // 將較小權值存入lowcost
                // 因爲選出的點離k最近,所以要更新一下
                adjvex[j] = k;    // 將下標爲k的頂點存入adjvex
            }
        }
    }
}

時間複雜度O[n2]O[n^2]


Kruskal算法

Prim算法是從頂點入手的,而Kruskal是從邊開始入手,這裏用到了邊集數組結構

邊的結構代碼

typedef struct Edge
{
    int begin;
    int end;
    int weight;
};

將邊按權重排序:

算法流程

  1. 建一個parent數組,用來判斷是否構成環路
  2. 每條邊遍歷
  3. 檢測是否構成環,沒有的話就用邊的頭作下標,尾作值,放到parent中

案例
按照代碼來,到了i=6的時候,已經獲得的邊如圖粗體所示(24和26權重的邊應該是印錯了):

在這之前都是n!=m,這是相當於兩個連通的邊集合納入到了最小生成樹中;
i = 7時,到了邊(v5,v6)(v_5,v_6),此時兩個Fine都會返回6,可知(v5,v6)(v_5,v_6)會在A中構成一個環路,所以不要這條邊;
i = 8時,到了邊(v1,v2)(v_1,v_2)也會讓A構成環路,所以也不要;
i = 9時,連接(v6,v7)(v_6,v_7),此後的均會構成環路,最小生成樹此時得到:

思路
直接從邊入手,找最小的,把所有的構不成環路的最小的連接起來,前面的連接是沒有問題的,到了n=m的時候,此時這個邊想進去集合已經來不及了,有跟他作用相同但是更短的邊在裏面。

時間複雜度O[eloge]O[e\log e]

兩種方法對比

  • Kruskal主要針對邊展開,邊數少的時候效率高,所以對稀疏圖有很大優勢
  • Prim對稠密圖,邊很多的情況會更好些

JAVA實現Prim算法

public class MinTree {

    public static void main(String[] args) {
        VertexList[] vertexLists = new VertexList[10];
        int[][] edgelist = new int[16][3];
        int[][] edgeMat = new int[10][10];
        InitVerAjd(vertexLists, edgelist);
        CreateAdjMat(edgelist, edgeMat);
        // Prim需要用到鄰接權值矩陣
        // 權值矩陣是沒有問題的
        // for (int i = 1; i < 10; i++) {
        //     System.out.println("第 " + i + "行");
        //     for (int j = 1; j < 10; j++){
        //         System.out.println(edgeMat[i][j]);
        //     }
        // }
        // Prim(vertexLists, edgeMat);
        Kruskal(vertexLists, edgelist, edgeMat);

    }

    private static void Prim(VertexList[] vertexLists, int[][] edgeMat) {
        int len = vertexLists.length;
        int[] adjvex = new int[len];
        int[] lowcost = new int[len];
        for (int i = 1; i < len; i++) {
            // 賦值第一個點的鄰接邊的權重
            // 這裏搞錯了
            lowcost[i] = edgeMat[1][i];
            // 初始化爲1, 就是生活樹中只有第一個頂點
            adjvex[i] = 1;
        }
        // 設爲0表示已經進樹了
        lowcost[1] = 0;
        // 每次選出一個頂點
        for (int i = 1; i < len-1; i++) {
            int min = Integer.MAX_VALUE;
            int k = 0;
            // 不會算第一個點本身
            for (int j = 2; j < len; j++) {
                if (lowcost[j]!=0 && lowcost[j]<min){
                    min = lowcost[j];
                    k = j;
                }
            }
            // 該點被選出
            lowcost[k] = 0;
            System.out.println("第" + i + "條邊:" + adjvex[k] + "->" + k);
            // 更新lowcost和adjvex
            for (int m = 2; m < len; m++) {
                // 看有無更近
                if (lowcost[m]!=0 && lowcost[m]>edgeMat[k][m]){
                    // 更近就更新
                    lowcost[m] = edgeMat[k][m];
                    // 得記錄和那個更近
                    adjvex[m] = k;
                }
            }
        }
    }

    // 這次的矩陣帶全值了
    public static void CreateAdjMat(int[][] edgeList, int[][] edgeMat) {
        // 長度搞錯了
        int lenVer = edgeMat.length;
        for (int i = 1; i < lenVer; i++) {
            for (int i1 = 1; i1 < lenVer; i1++) {
                // 不相連的距離設爲無窮大
                edgeMat[i][i1] = Integer.MAX_VALUE;
            }
        }
        int len = edgeList.length;
        for (int i = 1; i < len; i++) {
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            edgeMat[s][e] = edgeList[i][2];
            edgeMat[e][s] = edgeList[i][2];
        }
    }

    private static void InitVerAjd(VertexList[] vertexList, int[][] edgeList) {
        vertexList[1] = new VertexList('A',0);
        vertexList[2] = new VertexList('B',0);
        vertexList[3] = new VertexList('C',0);
        vertexList[4] = new VertexList('D',0);
        vertexList[5] = new VertexList('E',0);
        vertexList[6] = new VertexList('F',0);
        vertexList[7] = new VertexList('G',0);
        vertexList[8] = new VertexList('H',0);
        vertexList[9] = new VertexList('I',0);
        // 1
        edgeList[1][0] = 1;
        edgeList[1][1] = 2;
        edgeList[1][2] = 10;
        // 2
        edgeList[2][0] = 2;
        edgeList[2][1] = 3;
        edgeList[2][2] = 18;
        // 3
        edgeList[3][0] = 3;
        edgeList[3][1] = 4;
        edgeList[3][2] = 22;
        // 4
        edgeList[4][0] = 4;
        edgeList[4][1] = 5;
        edgeList[4][2] = 20;
        // 5
        edgeList[5][0] = 5;
        edgeList[5][1] = 6;
        edgeList[5][2] = 26;
        // 6
        edgeList[6][0] = 6;
        edgeList[6][1] = 1;
        edgeList[6][2] = 11;
        // 7
        edgeList[7][0] = 2;
        edgeList[7][1] = 7;
        edgeList[7][2] = 16;
        // 8
        edgeList[8][0] = 7;
        edgeList[8][1] = 6;
        edgeList[8][2] = 17;
        // 9
        edgeList[9][0] = 2;
        edgeList[9][1] = 9;
        edgeList[9][2] = 12;
        // 10
        edgeList[10][0] = 3;
        edgeList[10][1] = 9;
        edgeList[10][2] = 8;
        // 11
        edgeList[11][0] = 9;
        edgeList[11][1] = 4;
        edgeList[11][2] = 21;
        // 12
        edgeList[12][0] = 4;
        edgeList[12][1] = 8;
        edgeList[12][2] = 16;
        // 13
        edgeList[13][0] = 7;
        edgeList[13][1] = 4;
        edgeList[13][2] = 24;
        // 14
        edgeList[14][0] = 8;
        edgeList[14][1] = 5;
        edgeList[14][2] = 7;
        // 15
        edgeList[15][0] = 7;
        edgeList[15][1] = 8;
        edgeList[15][2] = 19;
    }
}

最短路徑

定義:對於網圖來說,最短路徑,是指兩頂點之間經過的邊上的權值之和最少的路徑,並且我們稱路徑上的第一個點是源點,最後一個頂點是終點。


Dijkstra算法

思路

  • 一步步求出所有頂點的最短路徑, 後面的頂點路徑尋找是建立在前面的尋找的基礎上的,通過不斷遍歷最近點的鄰接點來更新到v0點的距離。
  • 會進行很多次最近點的鄰接點和當付錢最近距離的比較,以此來找到最優解。


算法流程

  1. 新建三個數組,P,D,final,這三個的長度都是頂點數,P用來存放最短路徑,裏面的值指向最短路徑的前一個頂點;D中存放v0到下標位置點的最短路徑距離;final來記錄當前頂點是否已經算過最短路徑
  2. 初始化三個數組,D使用鄰接矩陣的v0行初始化,final全爲0表示沒被計算過,P全爲0表示未有路徑。
  3. 一個大循環,裏面套了兩個小循環,大循環對每個頂點進行遍歷
  4. 第一個小循環用來尋找D中未被計算過的最小距離和對應的頂點,用final來標記是否計算過,k值用來保存頂點位置
  5. 第二個小循環遍歷所有的頂點,讓v0到k點的最小距離+k到各個頂點的距離之前記錄D中的v0到各個頂點的距離比較,如果能更近就更新,同時保存路徑到P

實現代碼

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

// P[v]的值爲前驅頂點下標,D[v]表示v0到v的最短路徑長度和
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *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.matrix[v0][v];    // 和v0鄰接的頂點的權值
        (*P)[v] = 0;    // 初始化路徑數組P爲0
    }
    (*D)[v0] = 0;    // v0到v0的路徑
    final[v0] = 1;
    // 主循環,求v0到各個頂點的最短路徑
    for (v=1; v<G.numVertexes; v++)
    {
        min = INFINITY;
        for (w=0; w<G.numVertexes; w++)    // 尋找離v0最近的頂點
        {
            // 爲什麼不能重複找呢
            // 因爲如果不限定final的話,就一直是最小那個,無法向後推進
            if (!final[w] && (*D)[w]<min)    // 從未被找到的點裏找
            {
                k = w;
                min = (*D)[w];
            }
        }
        // 最近的點k被考慮過
        final[k] = 1;
        //
        for (w=0; w<G.numVertexes; w++)
        {
            // 如果經過k頂點的路徑比現在這條路徑的長度短的話
            // k是剩餘點中離v0最近的
            if (!final[w] && (min+G.matrix[k][w] < (*D)[w]))
            {
                // 說明找到了最短路徑,修改D[w]和P[w]
                (*D)[w] = min + G.matrix[k][w];
                (*P)[w] = k;
            }
        }
    }
}

結果圖

時間複雜度:遍歷嵌套遍歷,爲O[n2]O[n^2],如果要求所有頂點到所有頂點的最短路徑,就是O[n3]O[n^3]


Floyd算法

依次計算所有頂點經過某點後到達另一頂點的最短路徑。

思路

  • D數組存距離,P數組存路徑
  • 三個遍歷,K表示中轉頂點,考慮中轉比直線更近的情況;V表示起始頂點;W表示結束頂點
  • K在最外層,每個頂點之間都計算了中轉,這裏只要一箇中轉就能表達所有情況,因爲中轉的起點是之前計算出來的最短路徑,已經積累了很多中轉
  • 最裏層的循環判斷是否中轉更近,是的話就更新距離和路徑,讓原本值爲w的P[v][w]指向中轉點,即改變了起點的後期,把中轉點插到了原本的起點和終點之間,這樣起點指向的就是第一個中轉點了。
  • 最後的一個遍歷可讓路徑一直最短,從前到後一點點積累中轉點,到最後總有不可再插入中轉點的時候,此時path[k][w]=w,路徑的尋找就結束了。

最短路徑計算代碼實現

typedef int Pathmatrix[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];

void ShortestPath_Floyd(MGraph G, Pathmatrix *p, ShortPathTable *D)
{
    int v, w, k;
    for (v=0; v<G.numVertexes; ++v)
    {
        for (w=0; w<G.numVertexes; ++v)    // 初始化D與P
        {
            (*D)[v][w] = G.matrix[v][w];
            (*P)[v][w] = w;
        }
    }
    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])
                {
                    (*D)[v][w] = (*D)[v][k] + (*D)[k][w];
                    (*P)[v][w] = (*P)[v][k];    // 路徑設置爲中轉點k
                }
            }
        }
    }
}

求最短路徑
利用P來求出兩個點之間的最短路徑,比如求v0到v8,設P[0][8]=m,m爲0-8之間的一個點,然後再以m爲起點,得到P[m][8]=n,n是m和8之間的一箇中轉點,再以n爲起點,得到P[n][8]=…,這個過程中得到的點就是路徑。

最短路徑顯示代碼

for (v=0; v<G.numVertexes; ++v)
{
    for (w=v+1; w<G.numVertexews; w++)
    {
        // 求v和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\n", w);
    }
    printf("\n");
}

使用環境
要求所有頂點到所有頂點的最短路徑問題時,使用Floyd。


JAVA實現Dijkstra和Floyd算法

注意:設置最大值的時候不要使用數據類型的最大值(如Integer.MAX_VALUE),因爲又可能出現min+Integer.MAX_VALUE數據溢出變成負數的情況。

public class ShortesPath {
    public static void main(String[] args) {
        VertexList[] vertexLists = new VertexList[10];
        int[][] edgeList = new int[17][3];
        int[][] edgeMat = new int[10][10];
        InitShortPathVerAjd(vertexLists, edgeList);
        utils.CreateAdjMat(edgeList, edgeMat);
        // Dijkstra算法
        Dijkstra(edgeMat);
        // Floyd算法
        Floyd(edgeMat);
    }

    private static void Floyd(int[][] edgeMat) {
        int len = edgeMat.length;
        // 距離數組和路徑數組,因爲這次是兩層遍歷方法,所以用二維數組
        int[][] Dis = edgeMat.clone();
        int[][] Path = new int[len][len];
        // 初始化Path爲終點
        for (int v = 1; v < len; v++) {
            for (int w = 1; w < len; w++) {
                // v表示起點,w表示終點
                Path[v][w] = w;
            }
        }
        // k爲中轉點
        for (int k = 1; k < len; k++) {
            // 所有的頂點之間都做中轉
            for (int v = 1; v < len; v++) {
                for (int w = 1; w < len; w++) {
                    // 如果中專的距離更近,就更新距離和路徑爲中賺
                    if (Dis[v][w] > Dis[v][k]+Dis[k][w]){
                        Dis[v][w] = Dis[v][k]+Dis[k][w];
                        // 相當於改變了起點的後驅,把中轉點插到了原本的起點和頂點之間
                        Path[v][w] = Path[v][k];
                    }
                }

            }
        }
        FloydForeach(Path);
    }

    private static void FloydForeach(int[][] path) {
        int v = 1;
        int w = 8;
        int k = path[v][w];
        System.out.println(v + "到" + w + "的最短路徑");
        System.out.printf((k-1)+" ");
        while (k!=w){
            // 起點隨着中轉帶你一直在變,終點不變
            k = path[k][w];
            System.out.printf("-> " + (k-1) + " ");
        }
        System.out.println("-> " + k);
    }

    private static void Dijkstra(int[][] edgeMat) {
        int len = edgeMat.length;
        for (int i = 1; i < len; i++) {
            for (int j = 1; j < len; j++) {
                System.out.printf(edgeMat[i][j] + " ");
            }
            System.out.println("");
        }
        System.out.println("----------------");
        // 創建數組,假設下標從1開始
        int[] Final = new int[len];
        int[] Dis = new int[len];
        int[] Path = new int[len];
        // 初始化
        for (int i = 1; i < len; i++) {
            // 全部爲未知
            Final[i] = 0;
            // 以第一個頂點爲起點
            Dis[i] = edgeMat[1][i];
            // 路徑都設爲第一個頂點
            Path[i] = 1;
        }
        Final[1] = 1;
        Dis[1] = 0;
        // 大遍歷
        for (int i = 1; i < len; i++) {
            // 找離v0最近的沒被考慮過的點
            int min = 65535;
            // 用來記錄最近
            int k = 0;
            for (int w = 1; w < len; w++) {
                if (Final[w]==0 && Dis[w] < min){
                    k = w;
                    min = Dis[w];
                }
            }
            // 設k頂點爲已被考慮過
            Final[k] = 1;
            // 看是否能通過k更近
            for (int w = 1; w < len; w++) {
                if (Final[w]==0 && (min+edgeMat[k][w] < Dis[w])){
                    // 更近就更新距離和路徑
                    System.out.println("更新前到頂點"+w +"的距離: " + Dis[w]);

                    Dis[w] = min + edgeMat[k][w];
                    System.out.println("更新後到頂點"+w +"的距離: " + Dis[w]);
                    // 更新前驅點,通過k連接w會更近
                    Path[w] = k;
                }
            }
        }
        for (int i = 1; i < len; i++) {
            System.out.println(i-1+"的前驅是"+(Path[i]-1));
        }
    }

    private static void InitShortPathVerAjd(VertexList[] vertexList, int[][] edgeList) {
        vertexList[1] = new VertexList('A',0);
        vertexList[2] = new VertexList('B',0);
        vertexList[3] = new VertexList('C',0);
        vertexList[4] = new VertexList('D',0);
        vertexList[5] = new VertexList('E',0);
        vertexList[6] = new VertexList('F',0);
        vertexList[7] = new VertexList('G',0);
        vertexList[8] = new VertexList('H',0);
        vertexList[9] = new VertexList('I',0);
        // 1
        edgeList[1][0] = 1;
        edgeList[1][1] = 0;
        edgeList[1][2] = 1;
        // 2
        edgeList[2][0] = 0;
        edgeList[2][1] = 2;
        edgeList[2][2] = 5;
        // 3
        edgeList[3][0] = 1;
        edgeList[3][1] = 2;
        edgeList[3][2] = 3;
        // 4
        edgeList[4][0] = 1;
        edgeList[4][1] = 3;
        edgeList[4][2] = 7;
        // 5
        edgeList[5][0] = 1;
        edgeList[5][1] = 4;
        edgeList[5][2] = 5;
        // 6
        edgeList[6][0] = 2;
        edgeList[6][1] = 4;
        edgeList[6][2] = 1;
        // 7
        edgeList[7][0] = 2;
        edgeList[7][1] = 5;
        edgeList[7][2] = 7;
        // 8
        edgeList[8][0] = 3;
        edgeList[8][1] = 4;
        edgeList[8][2] = 2;
        // 9
        edgeList[9][0] = 4;
        edgeList[9][1] = 5;
        edgeList[9][2] = 3;
        // 10
        edgeList[10][0] = 3;
        edgeList[10][1] = 6;
        edgeList[10][2] = 3;
        // 11
        edgeList[11][0] = 4;
        edgeList[11][1] = 6;
        edgeList[11][2] = 6;
        // 12
        edgeList[12][0] = 4;
        edgeList[12][1] = 7;
        edgeList[12][2] = 9;
        // 13
        edgeList[13][0] = 7;
        edgeList[13][1] = 5;
        edgeList[13][2] = 5;
        // 14
        edgeList[14][0] = 6;
        edgeList[14][1] = 7;
        edgeList[14][2] = 2;
        // 15
        edgeList[15][0] = 6;
        edgeList[15][1] = 8;
        edgeList[15][2] = 7;
        // 16
        edgeList[16][0] = 7;
        edgeList[16][1] = 8;
        edgeList[16][2] = 4;

        // 搞錯了,應從1開始
        for (int i = 0; i < 16; i++) {
            edgeList[i+1][0] += 1;
            edgeList[i+1][1] += 1;
        }
    }
}

拓撲排序

AOV網:在一個表示工程的有向圖中,用頂點表示活動,用弧表示活動之間的優先關係,這樣的有向圖爲頂點表示活動的網,我們稱爲AOV網(Activity On Vertex Network)。
AOV網中的弧表示活動之間存在的某種制約關係,且不允許存在迴路,下面是一個簡單的例子:

拓撲序列

  • 設G=(V, E)是一個具有n個頂點的有向圖,V中的頂點是有序列的,如果滿足從viv_ivjv_j之間有一個條路經,則在頂點序列中iijj前面的化,就稱這樣的頂點序列爲一個拓撲序列。
  • 對於一個AOV網,拓撲序列可能不止一條。
  • 所謂的拓撲排序,其實就是一個有向圖構造拓撲序列的過程。

拓撲排序算法

對AOV網進行拓撲排序的思路:從AOV網中選擇一個入度爲0的頂點輸出,然後刪去此頂點和以此頂點爲尾的弧,繼續重複,直到輸出全部頂點或AOV網中不存在入度爲0的頂點爲止。

數據結構

AOV網

鄰接表數據結構

結構代碼

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

typedef struct VertexNode    // 頂點表結點
{
    int in;    // 頂點入度
    int data;    // 頂點域
    EgdeNode *firstedge;
}VertexNode, AdjList[MAXVEX];

typedef struct graphAdjList
{
    AdjList adjList;
    int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;

思路

  • 在算法中用棧來處理入度爲0的頂點,目的是爲了避免每個查找時都要遍歷找入度爲0的頂點。
  • 從入度爲0開始遍歷,入度爲0表示該點不會作爲弧頭,所以把這種點放在前面不會有問題,每打印一個,就相當於抹掉一個點,此時這個點的鄰接點的入度就-1,如果入度減到了0,那麼該點此時放入棧也是安全的,後面不會有讓該點作爲弧頭的弧。

理解

  • 感覺先輸出的都是後面的點啊?
    不是的,後面進去的都是通過前一個點(該點已被打印)的消除而入度爲0的,而且都是跟在後面的鄰接點。
  • 那麼最初的幾個入度爲0如何保證呢?還是說從弧找順序而不是從順序中找弧?
    應該是第一種,先給弧,再看順序,所以入度爲0的幾個順序無所謂。
// 拓撲排序,若GL無迴路,則輸出序列
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)    // 數量不夠,則存在環
        return ERROE;
    else
        return OK;
}

JAVA實現拓撲排序

頂點類

public class aovNode {
    public int in;
    public int data;
    public adjNode firstedge;


    public aovNode(int in, int data) {
        this.in = in;
        this.data = data;
    }
}

排序實現

public class TopologySort {
    public static void main(String[] args) {
        // 14個頂點
        int len = 6;
        aovNode[] verList = new aovNode[len+1];
        // 這裏是有向邊, 七條邊
        int[][] edgeList = new int[8][2];
        InitTopoplogy(verList, edgeList);
        CrateAdjList(verList, edgeList);
        Sort(verList, edgeList);
    }

    private static void Sort(aovNode[] verList, int[][] edgeList) {
        int len = verList.length;
        // 做一個棧存放入度爲0的點
        int[] stack = new int[len];
        int top = 0;
        int count = 0;
        int ver = 0;
        adjNode e = null;
        for (int i = 1; i < len; i++) {
            if (verList[i].in == 0){
                stack[top++] = i;
            }
        }
        // 開始遍歷,對點進行處理
        while (top != 0){
            // 在這裏打印
            ver = stack[--top];
            System.out.println(ver + "->");
            count++;
            // 對該點的鄰接點進行處理
            for (e = verList[ver].firstedge; e!=null; e=e.next){
                // 如果鄰接點的入度-1爲0的話,就入棧
                if ((--verList[e.data].in) == 0){
                    stack[top++] = e.data;
                }
            }
        }
        // 如果能輸出所有點,說明無環
        if (count == len-1){
            System.out.println("無環");
        } else {
            System.out.println("有環");
        }
    }

    private static void CrateAdjList(aovNode[] verList, int[][] edgeList) {
        int len = edgeList.length;
        for (int i = 1; i < len; i++) {
            int s = edgeList[i][0];
            int t = edgeList[i][1];
            adjNode adj = new adjNode();
            adj.data = t;
            adj.next = verList[s].firstedge;
            verList[s].firstedge = adj;
        }
    }

    private static void InitTopoplogy(aovNode[] verList, int[][] edgeList) {
        // 初始化頂點的值和入度
        verList[1] = new aovNode(0,1);
        verList[2] = new aovNode(1,2);
        verList[3] = new aovNode(2,3);
        verList[4] = new aovNode(2,4);
        verList[5] = new aovNode(2,5);
        verList[6] = new aovNode(0,6);
        // 初始化邊
        edgeList[1][0] = 1;
        edgeList[1][1] = 2;
        edgeList[2][0] = 1;
        edgeList[2][1] = 3;
        edgeList[3][0] = 2;
        edgeList[3][1] = 5;
        edgeList[4][0] = 3;
        edgeList[4][1] = 4;
        edgeList[5][0] = 5;
        edgeList[5][1] = 3;
        edgeList[6][0] = 5;
        edgeList[6][1] = 4;
        edgeList[7][0] = 6;
        edgeList[7][1] = 5;
    }
}

關鍵路徑

AOE網:在AOV網的弧上加上權值表示活動的持續時間,這樣的網叫做AOE網(Activity On Edge Network)。

路徑長度:路徑上各個活動所持續的時間之和。

關鍵路徑:從源點(入度爲0)到匯點(出度爲0)具有最大長度的路徑。

關鍵活動:在關鍵路徑上的活動。

算法原理
如果所有活動的最早開始時間和最晚開始時間相同,就意味着此活動爲關鍵活動,活動間的路徑爲關鍵路徑,若不等則不是。
定義如下參數:

  1. 事件的最早發生時間evt:即頂點vkv_k的最早發生時間
  2. 事件的最晚發生時間ltv:即頂點vkv_k的最晚發生時間,也就是每個頂點對應的時間最晚需要開始的時間,超出此時間將會延誤整個工期
  3. 活動的最早開工時間ete:即弧aka_k的最早發生時間
  4. 活動的最晚開工時間lte:即弧aka_k的最晚發生時間,也就是不推遲同期的最晚開工時間
    由1和2可以求得3和4,然後再根據ete[k]是否與lte[k]相等來判斷aka_k是否是關鍵活動。

關鍵路徑算法

全局變量

  • 求etv就是從頭到尾找拓撲序列的過程
  • stack2用來存儲拓撲序列
int *etv, *ltv;    // 事件最早發生時間和最遲發生時間數組
int *stack2;    // 用於存儲拓撲排序的棧,喫stack吐出來的
int top2;    // 用於stack2的指針

改進後的求拓撲序列算法
關鍵點在於最後的求各頂點事件最早發生時間值,鄰接點作爲弧的尾點,開始時間取的是各個弧頭和弧長的最大值,因爲只有前置事件都完成了,才能進入下一事件。

Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i, k, gettop;
    int top = 0;
    int count = 0;
    int *stack;
    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;
    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].firstedeg; e; e=e->next)
        {
            k = e->adjvex;
            if (!(--GL->adjList[k].in))
                stack[++top] = k;
            // 求各頂點事件最早發生時間值
            // 這裏的意思就是k要等前面忙完,取前面的最充裕時間
            if ((etv[gettop] + e->weight) > etv[k])
                etv[k] = etv[gettop] + e->weight;
        }
    }
    if (count < GL->numVertexes)
        return ERROR;
    else
        return OK;
}

關鍵路徑代碼
注意:

  • 鄰接表裏的weight是對應的弧的值,也就是兩點間的時間。
  • 一是求最晚發生時間,這裏是倒序,從最後的點開始,取的是最小最晚時間,也就是讓上一個活動開始的儘可能早,因爲上個活動結束後剩下的時間裏,要保證接下來的時間夠完成所有任務,而不是隻完成剩下的任務中耗時最短的那個,所以必須保證剩下的任務中耗時最長的可以完成,所以這裏用的是小於號,意思就是最晚中的最早。
  • 求lte的時候,當ete=lte時,說明這個任務線是時間最緊的,最早最晚時間是被這個任務約束的,而不等的時候,一定是 lte>ete 的,這種情況的任務執行的時候,時間是有剩餘的。
void CriticalPath(GraphAdjList GL)
{
    EgdeNode *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]while (top2 != 0)    // 計算ltv
    {
        gettop = stack2[top2--];    // 拓撲序列後進先出
        for (e=GL->adjList[gettop].firstedge; e; e=e->next)
        {
            // 求各頂點時間的最遲發生時間ltv的值
            k = e->adjvex;
            // 求各頂點事件最晚發生時間ltv
            // 這裏是要比較最晚開始的,因爲可能好多任務交叉的,所以一定要比較,而不是用減法往前推就行
            // 這裏要用最小,因爲如果用最大,有些任務就完不成了
            if (ltv[k]-e->weight < ltv[gettop])
                ltv[gettop] = ltv[k] - e->weight;
        }
    }
    // 求ete,lte和關鍵活動
    // 有了最早和最晚,遍歷所有點和其鄰接點比較即可
    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("<v%d, v%d>length: %d ",
                GL->adjList[j].data, GL->adjList[k].data, e->weight);
        }
    }
}

JAVA實現關鍵路徑

結點類

public class adjWNode {
    int data;
    int weight;
    adjWNode next;

    public adjWNode(int data, int weight) {
        this.data = data;
        this.weight = weight;
    }
}

class aovNode{
    public int in;
    public int data;
    public adjWNode firstedge;


    public aovNode(int in, int data) {
        this.in = in;
        this.data = data;
    }
}

主代碼

package Graph.KeyPath;

public class KeyPath {
    public static void main(String[] args) {
        aovNode[] verList = new aovNode[10];
        int[][] edgeList = new int[13][3];
        InitKeyPath(verList, edgeList);
        CreateAdjList(verList, edgeList);
        // 棧存放排序,這裏要用到從後往前的方法,所以用棧來存儲
        int[] stack = new int[10];
        // 記錄各個活動最早開始時間的數組
        int[] evt = new int[10];
        // 用新的拓撲排序來初始化上面兩個數組
        // top爲stack數組的top
        int top = TopologySave(verList, stack, evt);
        keyPath(stack, top, evt, verList);
    }

    private static void keyPath(int[] stack, int top, int[] evt, aovNode[] verList) {
        int len = verList.length;
        // 先求一波最晚時間
        int[] ltv = new int[len];
        // 初始化爲最大時間
        for (int i = 0; i < len; i++) {
            ltv[i] = evt[len-1];
        }
        // 從後往前
        while (top != 0){
            int gettop = stack[--top];
            // 對鄰接點下手
            for (adjWNode e=verList[gettop].firstedge; e!=null; e=e.next){
                // 更新時間,越晚越好
                // 鄰接點 - 路徑時間 最小,爲了保證所有任務都能完成
                if (ltv[e.data] - e.weight < ltv[gettop]){
                    ltv[gettop] = ltv[e.data] - e.weight;
                }
            }
        }
        // 最早和最晚都有了,遍歷比較就行了
        // 這樣可得到關鍵點
        for (int i = 0; i < len; i++) {
            if (evt[i] == ltv[i]){
                System.out.println(i);
            }
        }
        System.out.println("---------");

        // 這樣可得到路徑
        for (int i = 0; i < len; i++) {
            for (adjWNode e=verList[i].firstedge; e!=null; e=e.next){
                if (evt[i] == ltv[e.data]-e.weight){
                    System.out.println(i + "->" + e.data);
                }
            }
        }

    }

    private static int TopologySave(aovNode[] verList, int[] stack2, int[] evt) {
        int len = verList.length;
        int[] stack = new int[len];
        int count = 0;
        int top = 0;
        int top2 = 0;
        for (int i = 0; i < len; i++) {
            if (verList[i].in == 0){
                stack[top++] = i;
            }
        }
        // 初始化evt
        for (int i = 0; i < len; i++) {
            evt[i] = 0;
        }
        while(top!=0){
            int gettop = stack[--top];
            count++;
            // 存起來
            stack2[top2++] = gettop;
            for (adjWNode e=verList[gettop].firstedge; e!=null; e=e.next){
                if((--verList[e.data].in)==0){
                    stack[top++] = e.data;
                }
                // 這裏還得記下最早時間
                // e.weight就是從gettop到e.data需要的時間
                if (evt[gettop] + e.weight > evt[e.data]){
                    evt[e.data] = evt[gettop] + e.weight;
                }
            }
        }
        if (count == len){
            System.out.println("無環");
        } else {
            System.out.println("有環");
        }
        return top2;
    }

    private static void CreateAdjList(aovNode[] verList, int[][] edgeList) {
        for (int i=0; i< edgeList.length; i++){
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            adjWNode node = new adjWNode(e, edgeList[i][2]);
            node.next = verList[s].firstedge;
            verList[s].firstedge = node;
        }
    }

    private static void InitKeyPath(aovNode[] verList, int[][] edgeList) {
        verList[0] = new aovNode(0,0);
        verList[1] = new aovNode(1,1);
        verList[2] = new aovNode(1,2);
        verList[3] = new aovNode(2,3);
        verList[4] = new aovNode(2,4);
        verList[5] = new aovNode(1,5);
        verList[6] = new aovNode(1,6);
        verList[7] = new aovNode(2,7);
        verList[8] = new aovNode(1,8);
        verList[9] = new aovNode(2,9);

        edgeList[0][0] = 0;
        edgeList[0][1] = 1;
        edgeList[0][2] = 3;
        edgeList[1][0] = 0;
        edgeList[1][1] = 2;
        edgeList[1][2] = 4;
        edgeList[2][0] = 1;
        edgeList[2][1] = 3;
        edgeList[2][2] = 5;
        edgeList[3][0] = 1;
        edgeList[3][1] = 4;
        edgeList[3][2] = 6;
        edgeList[4][0] = 2;
        edgeList[4][1] = 3;
        edgeList[4][2] = 8;
        edgeList[5][0] = 2;
        edgeList[5][1] = 5;
        edgeList[5][2] = 7;
        edgeList[6][0] = 3;
        edgeList[6][1] = 4;
        edgeList[6][2] = 3;
        edgeList[7][0] = 4;
        edgeList[7][1] = 6;
        edgeList[7][2] = 9;
        edgeList[8][0] = 4;
        edgeList[8][1] = 7;
        edgeList[8][2] = 4;
        edgeList[9][0] = 5;
        edgeList[9][1] = 7;
        edgeList[9][2] = 6;
        edgeList[10][0] = 6;
        edgeList[10][1] = 9;
        edgeList[10][2] = 2;
        edgeList[11][0] = 7;
        edgeList[11][1] = 8;
        edgeList[11][2] = 5;
        edgeList[12][0] = 8;
        edgeList[12][1] = 9;
        edgeList[12][2] = 3;
    }
}

思維導圖

在這裏插入圖片描述

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