(C++)数据结构课程笔记7/9 - 图

230 阅读11分钟

§7 - 图

1 - 图的定义

相关概念

  • 图(Graph) 是由一个顶点(Vertex) 集 V 和一个弧(Arc) 集 VR 构成的数据结构
  • 弧是有方向的,因此图为有向图
  • 若有某顶点到另一顶点的弧必有另一顶点到该顶点的弧,则两弧表示两顶点之间的一条边,此时图为无向图(以后弧或边统称为弧)
  • 假设图有 n 个顶点,e 条弧:
    • 完全有向图e=An2=n(n1)e=A_n^2=n(n-1)
    • 完全无向图e=Cn2=n(n1)/2e=C_n^2=n(n-1)/2
  • 一个图的顶点集和弧集分别是另一个图的顶点集和弧集的子集,该图称为另一个图的子图
  • 有向图: = 出度 + 入度;无向图:度
  • 有时图的弧具有与它相关的数,称为,权可以表示从一个顶点到另一个顶点的距离或耗费,带权图称为
  • 路径回路有向路径
  • 在无向图中,若图中任意两个顶点之间都有路径相通,则称此图为连通图,否则称之为非连通图(在非连通图中定义连通分量
  • 在有向图中,若图中任意两个顶点之间都有有向路径相通,则称此图为强连通图
  • 假设连通图有 n 个顶点和 e 条边,其中 n-1 条边和 n 个顶点构成一个极小连通子图,称为此连通图的生成树

抽象数据类型

ADT Graph {
	数据对象V:
		V是具有相同特性的数据元素的集合,称为顶点集。
	数据关系VR:
		VR={<v,w>|v,w∈V且P(v,w)}
		<v,w>表示从v到w的一条弧,谓词P(v,w)定义了弧<v,w>的意义或信息。
    基本操作:
    	// 图的建立和销毁
    	CreateGraph(&G,V,VR)
    	DestroyGraph(&G)
    	// 插入或删除顶点
		InsertVex(&G,v) // 在图G中插入顶点v
		DeleteVex(&G,v) // 删除顶点v及其相关的弧
		// 插入和删除弧
		InsertArc(&G,v,w) // 在图G中增添弧<v,w>,若图G是无向的,则还增添对称弧<w,v>
		DeleteArc(&G,v,w) // 在图G中删除弧<v,w>,若图G是无向的,则还删除对称弧<w,v>
		// 对顶点的访问操作
		LocateVex(G,v) // 返回顶点位置信息
		GetVex(G,v) // 返回顶点值
		PutVex(&G,v,value) // 对顶点赋值
		// 对邻接点的操作
		FirstAdjVex(G,v) // 返回顶点v的第一个邻接点,若顶点v没有邻接点,则返回空
		NextAdjVex(G,v,w) // 返回顶点v相对于w的下一个邻接点,若w是v的最后一个邻接点,则返回空
		// 遍历
		DFSTraverse(G,v,visit()) // 从顶点v起深度优先遍历图G
		BFSTraverse(G,v,visit()) // 从顶点v起广度优先遍历图G
} ADT Graph

2 - 图的存储

邻接矩阵(Adjacent matrix)

用一个二维数组来表示顶点间的相邻关系

有向图的邻接矩阵

有 n(n≥1) 个顶点的有向图的邻接矩阵是具有如下性质的 n 阶方阵:

  • A[i][j] = 1,当 <v_i, v_j> ∈ VR
  • A[i][j] = 0,当 <v_i, v_j> ∉ VR

有向图的邻接矩阵不一定是对称方阵,邻接矩阵第 i 行的元素之和为顶点 v_i 的出度,邻接矩阵第 i 列的元素之和为顶点 v_i 的入度

无向图的邻接矩阵

有 n(n≥1) 个顶点的无向图的邻接矩阵是具有如下性质的 n 阶方阵:

  • A[i][j] = A[j][i] = 1,当 (v_i, v_j) ∈ VR
  • A[i][j] = A[j][i] = 0,当 (v_i, v_j) ∉ VR

无向图的邻接矩阵是对称方阵,邻接矩阵第 i 行(或第 i 列)的元素之和为顶点 v_i 的度

网的邻接矩阵

有 n(n≥1) 个顶点的有向网的邻接矩阵是具有如下性质的 n 阶方阵:

  • A[i][j] = w_ij,当 <v_i, v_j> ∈ VR
  • A[i][j] = ∞,当 <v_i, v_j> ∉ VR
代码
const int MAX_VERTEX_NUM=20;
const int INFINITY=0x3f3f3f3f;

typedef enum{DG,DN,UDG,UDN} GraphKind; // {有向图,有向网,无向图,无向网}

// 弧的定义
typedef struct {
    int adj; // 对图,为1或0,表示是否相邻;对网,为权值
    InfoType* info; // 该弧相关信息的指针
} ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

// 图的定义
typedef struct {
    GraphKind kind;
    T vexs[MAX_VERTEX_NUM];
    AdjMatrix arcs;
    int vexnum,arcnum;
} MGraph;
优缺点
  • 优点:省时间(容易判定任意两个顶点之间是否有弧相连或其权值,容易求得各个顶点的度)
  • 缺点:费空间(占用空间大小只与图中顶点个数有关,而与边的数目无关)

邻接表(Adjacent list)

对图中每个顶点建立一个单链表,每个单链表的结点表示该顶点的所有邻接顶点(在头结点和第一个表结点之间插入新的表结点,在最后插入新的表结点容易出现“无效的指针赋值”的错误)

有向图的邻接表

在有向图的邻接表中,其中一个链表的表结点数是顶点的出度

在有向图的逆邻接表中,其中一个链表的表结点数是顶点的入度

无向图的邻接表

在无向图的邻接表中,其中一个链表的表结点数是顶点的度

代码
const int MAX_VERTEX_NUM=20;

typedef enum{DG,DN,UDG,UDN} GraphKind; // {有向图,有向网,无向图,无向网}

// 弧的结点结构(表结点)
typedef struct ArcNode {
    int adjvex;
    struct ArcNode* nextarc;
    InfoType* info; // 该弧相关信息的指针(如权值)
} ArcNode;

// 顶点的结点结构(头结点)
typedef struct {
    T vex;
    ArcNode* firstarc;
} VNode,AdjList[MAX_VERTEX_NUM];

// 图的定义
typedef struct {
    GraphKind kind;
    AdjList vexs;
    int vexnum,arcnum;
} LGraph;
优缺点
  • 优点:省空间(占用空间大小取决于边的数目)
  • 缺点:费时间(要判定任意两个顶点之间是否有弧相连时,需扫描链表)

有向图的十字链表

十字链表是将有向图的邻接表和逆邻接表结合起来得到的一种链表

const int MAX_VERTEX_NUM=20;

// 弧的结点结构(表结点)
// |弧尾顶点位置|弧头顶点位置|指向下一个有相同弧尾的结点的指针|指向下一个有相同弧头的结点的指针|弧的相关信息|
typedef struct ArcBox {
    int tailvex,headvex;
    struct ArcBox *tlink,*hlink;
	InfoType* info;
} ArcBox;

// 顶点的结点结构(头结点)
// |顶点数据|指向该顶点的第一条入弧|指向该顶点的第一条出弧|
typedef struct {
    T data;
    ArcBox *firstin,*firstout;
} VexNode;

// 图的定义
typedef struct {
    VexNode vexs[MAX_VERTEX_NUM];
    int vexnum,arcnum;
} OLGraph;

3 - 图的遍历

图的遍历要比树的遍历复杂得多,因为图中任一顶点都可能和其余顶点相邻接。为了避免同一顶点被多次访问,在遍历过程中,必须记下每个已访问过的顶点。为此,可设一个标志数组 visited,初始化为 0,一旦访问了顶点 viv_i,便置 visited[i] 为 1 或被访问时的次序号。

深度优先搜索(Depth First Search)

连通图:从图中某个顶点 v0v_0 出发,访问此顶点,然后依次从 v0v_0 的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和 v0v_0 有路径相通的顶点都被访问到

// 从顶点v出发,深度优先搜索遍历连通图G
void DFS(Graph G,int v) {
    visit(v);
    visited[v]=1;
    for (int w=FirstAdjVex(G,v);w!=-1;w=NextAdjVex(G,v,w))
        if (!visited[w])
            DFS(G,w);
}

// 从顶点v出发,深度优先搜索遍历连通图G(邻接矩阵存储)
// 时间复杂度:O(n^2)
void DFS(MGraph G,int v) {
    visit(G.vexs[v]);
    visited[v]=1;
    for (int j=0;j<G.vexnum;++j)
        if (G.arcs[v][j]!=0&&!visited[j])
            DFS(G,j);
}

// 从顶点v出发,深度优先搜索遍历连通图G(邻接表存储)
// 时间复杂度:O(n+e),n为顶点数,e为弧数或两倍边数(通过遍历表结点查找所有顶点的邻接点所需时间为O(e),访问顶点所需时间为O(n))
void DFS(LGraph G,int v) {
    visit(G.vexs[v].vex);
    visited[v]=1;
    for (ArcNode* p=G.vexs[v].firstarc;p;p=p->nextarc)
        if (!visited[p->adjvex])
            DFS(G,p->adjvex);
}

非连通图:非连通图的遍历是一般情形,遍历非连通图就是多次遍历连通图

void DFSTraverse(Graph G) {
    for (int v=0;v<G.vexnum;++v)
        if (!visited[v])
            DFS(G,v);
}

广度优先搜索(Breadth First Search)

连通图:从图中的某个顶点 v0v_0 出发,并在访问此顶点之后依次访问 v0v_0 的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和 v0v0 有路径相通的顶点都被访问到(使用队列

非连通图:若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止

/*
时间复杂度
邻接矩阵存储:O(n^2)
邻接表存储:O(n+e)
*/
void BFSTraverse(Graph G) {
    Queue Q;
    InitQueue(Q);
    for (int v=0;v<G.vexnum;++v)
        if (!visited[v]) {
            visit(v);
            visited[v]=1;
            EnQueue(Q,v);
            while (!QueueEmpty(Q)) {
                int u=DeQueue(Q);
                for (int w=FirstAdjVex(G,u);w!=-1;w=NextAdjVex(G,u,w))
                    if (!visited[w]) {
                        visit(w);
                        visited[w]=1;
                        EnQueue(Q,w);
                    }
            }
        }
}

遍历算法的应用举例

求一条从顶点 v 到顶点 s 的简单路径 / 判断两顶点是否连通
  • 若存在从顶点 v 到顶点 s 的路径,则从顶点 v 出发进行 DFS,必能搜索到顶点 s
  • 遍历过程中搜索到的顶点不一定是路径上的顶点
void SearchPath(Graph G,int v,int s,char* PATH) {
    Append(PATH,GetVex(G,v));
    visited[v]=1;
    for (int w=FirstAdjVex(G,v);w!=-1&&!found;w=NextAdjVex(G,v,w))
        if (w==s) {
            found=1;
            Append(PATH,GetVex(G,w));
        }
    	else if (!visited[w])
            SearchPath(G,w,s,PATH);
    if (!found)
        Delete(PATH); // 删除路径上的点
}
最短路径问题
  • 只考虑不带权的图中两点之间的最短路径问题,即两顶点间所含弧的数目最少的路径就是最短路径(带权图中两点之间的最短路径问题见“5 - 最短路径”)

  • BFS 按路径长度渐增的次序遍历图,适合求最短路径问题

  • 使用链队列:

    1. 给链队列结点设置 next 和 prior 两个指针
    2. 修改入队操作:在队尾插入新结点,令其 prior 指针指向头结点
    3. 修改出队操作:仅移动队头指针,而不将队头结点从链表中删除
  • 例子:

4 - 最小生成树

从无向连通图中任一顶点开始遍历,遍历图时走过的边和图中所有顶点一起构成该连通图的极小连通子图(含义是:任何具有 n 个顶点的连通图,至少有 n-1 条边),称为该连通图的生成树(含义是:所有具有 n-1 条边的连通图都是树)

图的生成树不唯一,从不同顶点出发进行不同搜索,都可以得到不同的生成树

若为连通图的各条边赋权,表示相应的代价,则从连通图中选择一棵总代价最小的树,即为最小代价生成树

  • 经典算法:克鲁斯卡尔(Kruskal)算法普里姆(Prim)算法
  • 基本思想:贪心,按权值非递减次序构造最小代价生成树

Kruskal 算法

算法描述

先分离图的顶点和边,再尝试将边连接回各顶点。规则是:从权值最小的边开始,若它的添加不使现在的图产生回路,则添加这条边,否则丢弃;如此重复,直至添加 n-1 条边为止。

实现
/*
无向连通网的简单存储方法:
- 定义存储顶点数的变量vexnum和存储边数的变量arcnum
- 将图的顶点映射到0~vexnum-1
- 用Edge类型数组存储图的边(同一条边只存储一次)
*/
typedef struct {
    int vex1;
    int vex2;
    int w;
} Edge;

typedef struct {
    int vexnum,arcnum;
    Edge edge[MAX_EDGE_NUM];
} Graph;

// 用STL中的sort实现边数组元素按权值排序,以下为排序方式定义
struct less_weight {
    bool operator()(const Edge &a1,const Edge &a2)const {
        return a1.w<a2.w;
    }
};

// 主算法
Graph MiniSpanTree_K(Graph G) {
    Graph res;
    res.vexnum=G.vexnum;
    res.arcnum=G.vexnum?G.vexnum-1:0;
    
    sort(G.edge+0,G.edge+G.arcnum,less_weight());
    
    int vset[MAX_VERTEX_NUM]; // 辅助数组,用于判断回路(重要思路:集合的合并)
    for (int i=0;i<res.vexnum;++i)
        vset[i]=i;
    for (int i=0,j=0;i<G.arcnum;++i) { // i为G.edge下标,j为res.edge下标
        int tmp1=vset[G.edge[i].vex1];
        int tmp2=vset[G.edge[i].vex2];
        if (tmp1!=tmp2) {
            res.edge[j++]=G.edge[i];
            for (int k=0;k<res.vexnum;++k)
                if (vset[k]==tmp2)
                    vset[k]=tmp1;
        }
        if (j==res.arcnum)
            break;
    }
    return res;
}
分析

Kruskal 算法的时间复杂度就是将边数组元素按权值排序的时间复杂度,即 O(eloge),其中 e 为边数。因此,Kruskal 算法更适用于稀疏图。

Prim 算法

算法描述

取图中任意一个顶点 v 作为生成树的根,然后取图中与 v 直接连接且连接边权值最小的顶点 w 加入生成树;接着将生成树中已有的顶点看作整体,取图中与该整体直接连接且连接边权值最小的顶点加入生成树,循环上述过程直至生成树上含有 n 个顶点为止。设置两个集合分别表示生成树中已有的顶点和图中其他顶点。

实现

实现过程示例:从顶点 a 出发构造最小生成树(结合代码理解)

  • 初始化:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvex
    mst[i].lowcost
    mst[i].flag1000000
  • 第一轮:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvexaaa
    mst[i].lowcost191418
    mst[i].flag10000→100
  • 第二轮:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvexeeae
    mst[i].lowcost1281416
    mst[i].flag1000→1100
  • 第三轮:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvexddeade
    mst[i].lowcost738142116
    mst[i].flag100→11100
  • 第四轮:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvexcdeade
    mst[i].lowcost538142116
    mst[i].flag10→111100
  • 第五轮:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvexcdeade
    mst[i].lowcost538142116
    mst[i].flag1111100→1
  • 第六轮:

    G.vexs[i]a (0)b (1)c (2)d (3)e (4)f (5)g (6)
    mst[i].adjvexcdeade
    mst[i].lowcost538142116
    mst[i].flag111110→11
typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

typedef struct {
    T vexs[MAX_VERTEX_NUM];
    AdjMatrix arcs;
    int vexnum,arcnum;
} Graph;

struct {
    int adjvex; // 将图的顶点映射到0~vexnum-1
    int lowcost;
    bool flag;
} mst[MAX_VERTEX_NUM];

void MiniSpanTree_P(Graph G,T v) { // 从顶点v出发构造最小生成树
    // 初始化mst数组
    for (int i=0;i<G.vexnum;++i) {
        mst[i].lowcost=INFINITY;
        mst[i].flag=false;
    }
    int k=LocateVex(G,v);
    mst[k].flag=true;
    
    for (int t=1;t<G.vexnum;++t) { // t记轮数
        int min=INFINITY,minidx;
        for (int i=0;i<G.vexnum;++i) {
            if (!mst[i].flag&&G.arcs[k][i]<mst[i].lowcost) {
                mst[i].adjvex=k;
                mst[i].lowcost=G.arcs[k][i];
            }
            if (!mst[i].flag&&mst[i].lowcost<min) {
                min=mst[i].lowcost;
                minidx=i;
            }
        }
        mst[minidx].flag=true;
        k=minidx;
    }
}
分析

Prim 算法的时间复杂度是 O(n^2),其中 n 为顶点数。因此,Prim 算法更适用于稠密图。

其他算法

Kruskal 算法和 Prim 算法被称为避圈法,相应地,另一种破圈法的思路是:从大到小删除边并保证删除后仍是连通图,直至剩下 n-1 条边。

5 - 最短路径

经典算法:

  • 迪杰斯特拉(Dijkstra)算法:基于贪心思想,求从某顶点到其他各顶点的最短路径
  • 弗洛伊德(Floyd)算法:基于动态规划,求每一对顶点之间的最短路径

Dijkstra 算法:求从某顶点到其他各顶点的最短路径

示例

上图所示带权有向图中从 v0 到其他各顶点的最短路径为:

终点最短路径最短路径长度
v1-
v2(v0, v2)10
v3(v0, v4, v3)50
v4(v0, v4)30
v5(v0, v4, v3, v5)60

从 v0 到其他各顶点的最短路径中的最短者为从 v0 到 v2 的最短路径

算法描述

基本思想:按最短路径长度递增的次序产生最短路径

  • 最短路径中的最短者的特点:该路径上必定只有一条弧,且这条弧必定是从始点出发的弧中权值最小的,记作 (v0, vi)
  • 最短路径中的次短者的特点:(两种情况)或是直接从始点到该点,记作 (v0, vj)(从始点出发的弧中权值次小的弧);或是从始点经过顶点 vi 再到该点,记作 (v0, vi, vj)(从始点出发的弧中权值最小的弧 + 从顶点 vi 出发的弧中权值最小的弧)
  • 最短路径中的第三短者的特点:(三种情况)或是直接从始点到该点,记作 (v0, vk);或是从始点经过顶点 vi 再到该点,记作 (v0, vi, vk);或是从始点经过顶点 vj 再到该点,记作 (v0, ..., vj, vk)
  • 其余最短路径的特点:或是直接从始点到该点;或是从始点经过已求得最短路径的顶点,再到该点
实现

实现过程示例(与 Prim 算法很类似):求示例图中从 v0 到其他各顶点的最短路径(结合代码理解)

  • 初始化:

    G.vexs[i]v0v1v2v3v4v5
    path[i](v0)
    pathdist[i]0
    flag[i]100000
  • 第一轮:

    G.vexs[i]v0v1v2v3v4v5
    path[i](v0)(v0, v2)(v0, v4)(v0, v5)
    pathdist[i]01030100
    flag[i]100→1000
  • 第二轮:

    G.vexs[i]v0v1v2v3v4v5
    path[i](v0)(v0, v2)(v0, v2, v3)(v0, v4)(v0, v5)
    pathdist[i]0106030100
    flag[i]10100→10
  • 第三轮:

    G.vexs[i]v0v1v2v3v4v5
    path[i](v0)(v0, v2)(v0, v4, v3)(v0, v4)(v0, v4, v5)
    pathdist[i]010503090
    flag[i]1010→110
  • 第四轮:

    G.vexs[i]v0v1v2v3v4v5
    path[i](v0)(v0, v2)(v0, v4, v3)(v0, v4)(v0, v4, v3, v5)
    pathdist[i]010503060
    flag[i]101110→1
  • 第五轮:

    G.vexs[i]v0v1v2v3v4v5
    path[i](v0)(v0, v2)(v0, v4, v3)(v0, v4)(v0, v4, v3, v5)
    pathdist[i]010503060
    flag[i]10→11111
typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

typedef struct {
    T vexs[MAX_VERTEX_NUM];
    AdjMatrix arcs;
    int vexnum,arcnum;
} Graph;

int path[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
int pathdist[MAX_VERTEX_NUM];
bool flag[MAX_VERTEX_NUM];

void ShortestPath_D(Graph G,T v) {
    // 初始化
    for (int i=0;i<G.vexnum;++i) {
        pathdist[i]=INFINITY;
        flag[i]=false;
    }
    int k=LocateVex(G,v);
    path[k][0]=k;
    pathdist[k]=0;
    flag[k]=true;
    
    for (int t=1;t<G.vexnum;++t) { // t记轮数
        int min=INFINITY,minidx;
        for (int i=0;i<G.vexnum;++i) {
            if (!flag[i]&&pathdist[k]+G.arcs[k][i]<pathdist[i]) {
                int j=0;
                do {
                    path[i][j]=path[k][j];
                    ++j;
                }
                while (path[k][j-1]!=k);
                path[i][j]=i;
                pathdist[i]=pathdist[k]+G.arcs[k][i];
            }
            if (!flag[i]&&pathdist[i]<min) {
                min=pathdist[i];
                minidx=i;
            }
        }
        flag[minidx]=true;
        k=minidx;
    }
}
分析
  • 时间复杂度:O(n^2),其中 n 为顶点数
  • 局限:若图中含有负数权值,则无法保证按最短路径长度递增的次序产生最短路径,即 Dijkstra 算法很有可能失效;解决方法是:先将所有权值映射到正区间再使用 Dijkstra 算法,或采用改进的 Bellman-Ford 算法

Floyd 算法:求每一对顶点之间的最短路径

求每一对顶点之间的最短路径,解决方法是:每次以一个顶点为始点执行 n 次 Dijkstra 算法,或采用 Floyd 算法

算法描述

基本思想:在每一对顶点之间所有存在的路径中,选出一条长度最短的路径;关键是要确认两点间的所有路径都被比较过

  • 若从 vi 到 vj 的弧存在,则存在路径 (vi, vj)
  • 若路径 (vi, vk) 和 (vk, vj) 存在,则存在路径 (vi, vk, vj)
  • 若路径 (vi, ..., vk) 和 (vk, ..., vj) 存在,则存在路径 (vi, ..., vk, ..., vj)
  • 以此类推,从 vi 到 vj 的最短路径为上述这些路径中长度最短者
实现

实现过程示例:求图中每一对顶点之间的最短路径,用数组 pathdist 存储,pathdist[i][j] 表示从 vi 到 vj 的已比较的路径中的最短者(结合代码理解)

  • 初始化:

  • pathdist[i][j] 与路径 (vi, ..., v0, ..., vj) 的比较:

  • pathdist[i][j] 与路径 (vi, ..., v1, ..., vj) 的比较:

  • pathdist[i][j] 与路径 (vi, ..., v2, ..., vj) 的比较:

  • pathdist[i][j] 与路径 (vi, ..., v3, ..., vj) 的比较:

typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

typedef struct {
    T vexs[MAX_VERTEX_NUM];
    AdjMatrix arcs;
    int vexnum,arcnum;
} Graph;

int path[MAX_VERTEX_NUM][MAX_VERTEX_NUM][MAX_VERTEX_NUM];
int pathdist[MAX_VERTEX_NUM][MAX_VERTEX_NUM];

void ShortestPath_F(Graph G) {
    // 初始化
    for (int i=0;i<G.vexnum;++i)
        for (int j=0;j<G.vexnum;++j) {
            path[i][j][0]=i;
            path[i][j][1]=j;
            if (i==j)
                pathdist[i][j]=0;
            else
                pathdist[i][j]=G.arcs[i][j];
        }
    
    for (int k=0;k<G.vexnum;++k)
        for (int i=0;i<G.vexnum;++i)
            for (int j=0;j<G.vexnum;++j) {
                if (pathdist[i][k]+pathdist[k][j]<pathdist[i][j]) {
                    pathdist[i][j]=pathdist[i][k]+pathdist[k][j];
                    int t=0;
                    do {
                        path[i][j][t]=path[i][k][t];
                        ++t;
                    }
                    while (path[i][k][t-1]!=k);
                    int tmp=t;
                    do {
                        path[i][j][t]=path[k][j][t-tmp+1];
                        ++t;
                    }
                    while (path[k][j][t-tmp]!=j);
                }
            }
}
分析
  • 时间复杂度:O(n^3),其中 n 为顶点数
  • 允许有负数权值

6 - 拓扑排序

背景

定义

对于有向图,给出其顶点的线性序列 (v_0, v_1, ..., v_n-1),如果该线性序列满足:若图中从顶点 v_i 到顶点 v_j 有一条路径,则序列中顶点 v_i 必在顶点 v_j 之前,那么称该线性序列为拓扑序列;求有向图顶点的拓扑序列的过程,称作拓扑排序

应用

有向图可以表示工程的施工流程、产品的生产流程、学生的课程安排等:有向图的顶点表示一项子工程、一个子产品或一门课程,弧 <v_i, v_j> 表示子工程、子产品或课程 v_i 必须在 v_j 之前完成

对有向图的顶点进行拓扑排序,对应于这些实际问题就是为各项子工程、各个子产品或各门课程排出一个线性的顺序关系

若受条件限制这些工作必须串行,则应该按照拓扑序列安排执行的先后顺序

示例

某项工程的施工流程为:

  • 子工程 1:无条件
  • 子工程 2:在子工程 1, 3 之后
  • 子工程 3:在子工程 1 之后
  • 子工程 4:在子工程 1, 6 之后
  • 子工程 5:在子工程 3, 4, 6 之后
  • 子工程 6:无条件

要求为各项子工程排出一个线性的顺序关系

  1. 该工程的施工流程对应的有向图为:(唯一)

  2. 对有向图的顶点进行拓扑排序,得到的拓扑序列为:(不唯一且不一定存在)

    • 1, 3, 2, 6, 4, 5
    • 6, 1, 4, 3, 2, 5
    • ……
小结

用顶点表示活动,用弧表示活动间优先关系的有向无环图称为 AOV-网(Activity On Vertex network)

在 AOV-网中,要求不能出现有向环;因为有向环表示某项活动应以自己为先决条件,而显然这是荒谬的(死循环,拓扑序列不存在)

拓扑排序算法

算法描述
  1. 从图中选择一个入度为零的顶点加入序列
  2. 从图中删除此顶点及由它发出的弧
  3. 重复执行 1, 2,直至所有顶点加入序列(完成拓扑排序)或图中再也没有入度为零的顶点(图中存在有向环,拓扑序列不存在)

邻接矩阵上实现的具体算法:

设置 flag 数组(flag[i] = 0 表示顶点 vexs[i] 存在;flag[i] = 1 表示顶点 vexs[i] 被删除)

  • 选择一个入度为零的顶点:找全零的一列(还需根据 flag 数组判断顶点存在)
  • 删除此顶点及由它发出的弧:flag 数组对应元素置为 1;对应行置为全零

邻接表上实现的具体算法:

在头结点增加一个域,存放顶点入度;设置一个栈,存放入度为零的顶点

  1. 建立邻接表存储图
  2. 查找头结点向量中入度为零的顶点,将其压入栈
  3. 重复执行以下操作,直至栈空:
    • 栈顶元素 v 出栈,将其加入序列
    • 在头结点向量中查找 v 的所有邻接点,将其入度减 1,若其入度变成零,则将其压入栈
  4. 栈空时,若所有顶点都已加入序列,则完成拓扑排序,否则说明图中存在有向环,拓扑序列不存在
在邻接表上的实现
typedef struct ArcNode {
    int adjvex;
    struct ArcNode* nextarc;
} ArcNode;

typedef struct {
    T vex;
    int indegree;
    ArcNode* firstarc;
} VNode,AdjList[MAX_VERTEX_NUM];

typedef struct {
    AdjList vexs;
    int vexnum,arcnum;
} Graph;

// 若成功完成拓扑排序,则返回true;若图中存在有向环,拓扑序列不存在,则返回false
bool TopologicalSort(Graph G,T TopoSeq[]) {
    Stack S;
    InitStack(S);
    for (int i=0;i<G.vexnum;++i)
        if (G.vexs[i].indegree==0)
            Push(S,G.vexs[i].vex);
    int cnt=0;
    while (!StackEmpty(S)) {
        T v=Pop(S);
        TopoSeq[cnt++]=v;
        int k=LocateVex(G,v);
        for (ArcNode* p=G.vexs[k].firstarc;p;p=p->nextarc) {
            --G.vexs[p->adjvex].indegree;
            if (G.vexs[p->adjvex].indegree==0)
                Push(S,G.vexs[p->adjvex].vex);
        }
    }
    if (cnt<G.vexnum)
        return false;
    else
        return true;
}

7 - 关键路径

背景

AOE-网

用顶点表示事件,用弧表示活动,用弧上权值表示活动持续时间的带权有向无环图称为 AOE-网(Activity On Edge network);AOE-网可以用来估算工程完成时间

示例

某项工程的进度图如下:

入度为零的顶点称为源点,可以看作工程的开始点;出度为零的顶点称为汇点,可以看作工程的完成点;AOE-网通常只有一个源点和一个汇点

分析过程如下:

  1. 拓扑序列:1, 3, 2, 4, 5, 6

    逆拓扑序列:6, 5, 4, 2, 3, 1(两种求法:依次将出度为零的顶点加入序列;反写拓扑序列)

    拓扑序列中第一个顶点为源点,最后一个顶点为汇点

  2. 按照拓扑序列的顺序,计算各顶点所表示事件的最早发生时间 Ve(求法:木桶原理,约定源点的最早发生时间为 0,求所有指向某一事件的活动中,弧尾事件的最早发生时间与该活动持续时间之和的最大值)

    按照逆拓扑序列的顺序,计算各顶点所表示事件的最晚发生时间 Vl(求法:想象赶 DDL,约定汇点的最晚发生时间等于其最早发生时间,求所有指离某一事件的活动中,弧头事件的最晚发生时间与该活动持续时间之差的最小值)

    列下表:

    123456
    Ve01915293843
    Vl01915373843
  3. 计算各弧所表示活动的最早开始时间 e(弧尾事件的最早发生时间)

    计算各弧所表示活动的最晚开始时间 l(弧头事件的最晚发生时间与该活动持续时间之差)

    列下表:

    <1, 2><1, 3><2, 4><2, 5><3, 2><3, 5><4, 6><5, 6>
    e00191915152938
    l170271915273738
    l-e1708001280
示例引出的重要概念
  • 关键活动:l-e = 0 的活动

  • 关键路径:由关键活动组成的路径,关键路径长度等于汇点的发生时间,即完成工程的最短时间,如下图:

注意:

  1. 增加某一关键活动持续时间将增加关键路径长度,延长工期;因此,如果工期固定,那么关键活动的开始时间不能耽误
  2. 一定程度上,减少某一关键活动持续时间将减少关键路径长度,缩短工期;但减少某一关键活动持续时间可能改变关键路径,继续减少该活动持续时间将无法缩短工期
  3. 若有多条关键路径,则只使一条关键路径的长度减少将无法缩短工期,需使所有关键路径的长度同时减少

求关键路径算法

typedef struct ArcNode {
    int adjvex;
    int weight;
    int e;
    int l;
    int tag; // tag为1,该弧表示关键活动;tag为0,该弧表示非关键活动
    struct ArcNode* nextarc;
} ArcNode;

typedef struct {
    T vex;
    int indegree;
    ArcNode* firstarc;
} VNode,AdjList[MAX_VERTEX_NUM];

typedef struct {
    AdjList vexs;
    int vexnum,arcnum;
} Graph;

int Ve[MAX_VERTEX_NUM],Vl[MAX_VERTEX_NUM];

// 拓扑排序过程中完成各顶点Ve的计算,若成功完成拓扑排序,则返回true;若图中存在有向环,拓扑序列不存在,则返回false
bool TopologicalSort(Graph G,T TopoSeq[]) {
    Stack S;
    InitStack(S);
    for (int i=0;i<G.vexnum;++i)
        if (G.vexs[i].indegree==0)
            Push(S,G.vexs[i].vex);
    int cnt=0;
    while (!StackEmpty(S)) {
        T v=Pop(S);
        TopoSeq[cnt++]=v;
        int k=LocateVex(G,v);
        for (ArcNode* p=G.vexs[k].firstarc;p;p=p->nextarc) {
            --G.vexs[p->adjvex].indegree;
            if (G.vexs[p->adjvex].indegree==0)
                Push(S,G.vexs[p->adjvex].vex);
            // *****增加的代码:各顶点Ve的计算*****
            if (Ve[k]+p->weight>Ve[p->adjvex])
                Ve[p->adjvex]=Ve[k]+p->weight;
            // ********************************
        }
    }
    if (cnt<G.vexnum)
        return false;
    else
        return true;
}

// 若成功完成拓扑排序,则返回true;若图中存在有向环,拓扑序列不存在,则返回false
bool CriticalPath(Graph G) {
    T TopoSeq[MAX_VERTEX_NUM];
    if (!TopologicalSort(G,TopoSeq))
        return false;
    
    // 初始化各顶点Vl
    for (int i=0;i<G.vexnum;++i)
        Vl[i]=Ve[G.vexnum-1];
    
    // 各顶点Vl的计算
    int cnt=G.vexnum-1;
    while (cnt) {
        int k=LocateVex(G,TopoSeq[--cnt]);
        for (ArcNode* p=G.vexs[k].firstarc;p;p=p->nextarc)
            if (Vl[p->adjvex]-p->weight<Vl[k])
                Vl[k]=Vl[p->adjvex]-p->weight;
    }
    
    // 各弧e和l的计算,标识关键活动
    for (int i=0;i<G.vexnum;++i)
        for (ArcNode* p=G.vexs[i].firstarc;p;p=p->nextarc) {
            p->e=Ve[i];
            p->l=Vl[p->adjvex]-p->weight;
            if (p->e==p->l)
                p->tag=1;
            else
                p->tag=0;
        }
    return true;
}