数据结构_图

news2025/2/24 9:17:27

目录

1. 图的基本概念

1.1 图的定义

1.1.1 有向图

1.1.2 无向图

1.1.3 简单图、多重图

1.1.4 完全图(也称简单完全图)

1.1.5 子图

1.1.6 连通、连通图和连通分量

1.1.7 强连通图、强连通分量

1.1.8 生成树、生成森林

1.1.9 顶点的度、入度和出度

1.1.10 边的权和网

1.1.11 稠密图、稀疏图

1.1.12 路径、路径长度和回路

1.1.13 简单路径、简单回路

1.1.14 距离

1.1.15 有向树

1.2 相关练习

2. 图的存储及基本操作

2.1 邻接矩阵法

2.2 邻接表法

2.3 十字链表

2.4 邻接多重表

2.5 图的基本操作

2.6 相关练习

3. 图的遍历

3.1 广度优先搜索

3.1.1 BFS(广度优先搜索)的性能分析

3.1.2 BFS 算法求解单源最短路径问题

3.1.3 广度优先生成树

3.2 深度优先搜索

3.2.1 DFS(深度优先遍历)算法的性能分析

3.2.2 深度优先的生成树和生成森林

3.3 图的遍历与图的连通性

3.4 相关练习

4. 图的应用

4.1 最小生成树

4.1.1 Prim 算法

4.1.2 Kruskal 算法

4.2 最短路径

4.2.1 Dijkstra 算法求单源最短路径问题

4.2.2 Floyd 算法求各顶点之间最短路径问题

4.3 有向无环图描述表达式

4.4 拓扑排序

4.5 关键路径

4.6 相关练习


1. 图的基本概念

1.1 图的定义

        图 G 由 顶点集 V 和 边集 E 组成,记为 G = (V,E),其中 V(G) 表示图 G 中顶点的有限非空集E(G) 表示 图G 中顶点之间的关系 (边) 集合若 V = {v_{1}v_{2}……v_{n}},则用 |V| 表示 图G 中顶点的个数E = {(u,v)|u\epsilonV,v\epsilonV},用 |E| 表示 图G 中的条数

注意:

        线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集 V 一定非空,但边集 E 可以为空,此时图中只有顶点而没有边。

下面介绍图的基本概念及一些术语:

1.1.1 有向图

        若边集 E 是有向边(也称弧)的有限集合时,则图 G 为有向图是顶点的有序对,记为<v,w>,其中v,w是顶点,v 称为弧尾,w 称为弧头<v,w>称为从 v 到 w 的弧也称为 v 邻接到 w

上图中 有向图G1 可以表示为:

        G_{1} = (V_{1}E_{1})

        V_{1} = {1,2,3}

        E_{1} = {<1,2>,<2,1>,<2,3>}

1.1.2 无向图

        若 边集E 是无向边(简称边)的有限集合时,则图 G 为无向图边是顶点的无序对,记为(v,w)或(w,v)。可以说 w 和 v 互为邻接点。边(v,w)依附于 w 和 v,或称边(v,w)和 v,w 相关联。

上图中 无向图G2 可以表示为:

G_{2} = {V_{2}E_{2}}

V_{2} = {1,2,3,4}

E_{2} = {(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)}

1.1.3 简单图、多重图

一个图 G 如果满足:

        ① 不存在重复边;② 不存在顶点到自身的边,那么称 图G 为简单图。 上图中 有向图G_{1} 和 无向图G_{2} 均为简单图

        若 图G 中某两个顶点之间的边数大于 1 条,又允许顶点通过一条边和自身关联,则称 图G 为多重图

1.1.4 完全图(也称简单完全图)

        对于无向图边数 |E| 的取值范围为 0 到 n(n-1)/2(意思是就是任意两个结点之间都成产生一条边,C_{n}^{2}),有 n(n-1)/2 条边的无向图称为 完全图 ,在完全图中任意两个顶点之间都存在边。对于有向图边数 |E| 的取值范围为 0 到 n(n-1),有 n(n-1) 条弧的有向图称为 有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。无向图 G_{2} 为无向完全图。

1.1.5 子图

        设有两个图 G=(V,E) 和 G^{'} =(V^{'}E^{'}),若 V^{'} 是 V 的子集,且 E^{'} 是 E 的子集,则称 G^{'} 是 G 的子图若有满足 V(G^{'}) = V(G) 的子图 G^{'},则称其为 G 的生成子图

注意:

        并非 V 和 E 的任何子集都能构成 G 的子图,因为这样的子集可能不是图,即 E 的子集中的某些边关联的顶点可能不在这个 V 的子集中。

1.1.6 连通、连通图和连通分量

        在无向图中,若从顶点 v 到顶点 w 的路径存在,则称 v 和 w 是连通的。若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非连通图。无向图中的 极大连通子图 称为连通分量

如上图中 图6.2 a 为 无向图G_{4}图6.2 b 为其三个连通分量。

注:

        假设一个图有 n 个顶点,如果边数小于 n-1,那么此图必然是非连通图。

对于 n 个顶点的无向图 G:

        若 G 是连通图,则最少有 n-1 条边

        若 G 是非连通图,则最多可能有 C_{n-1}^{2} 条边

对于 n 个顶点的有向图 G:

        若 G 是强连通图,则最少有 n 条边(形成回路)

1.1.7 强连通图、强连通分量

        在有向图中,如果有一对顶点 v 和 w ,从 v 到 w 和从 w 到 v 之间都有路径,则称这两个顶点是强连通的。若图中任何一对结点都是强连通的,则称此图为 强连通图有向图中极大强连通子图称为有向图的强连通分量

1.1.8 生成树、生成森林

        连通图的生成树是包含图中全部顶点的一个极小连通子图若图中顶点数为 n ,则它的生成树含有 n-1 条边。包含图中全部顶点的极小连通子图,只有生成树满足这个极小条件。对生成树而言,若砍去它的一条边,则会变为非连通图,若加上一条边就会形成一个回路。在非连通图中,连通分量的生成树构成了非连通图的生成森林

注意:

        区分极大连通子图和极小连通子图:

        极大连通子图是无向图的连通分量,极大即要求该连通子图包含其所有的边;

        极小连通子图是既要保持图连通又要使得边数最少的子图;

1.1.9 顶点的度、入度和出度

        在无向图中顶点 v 的度是指依附于顶点 v 的边的条数,记为 TD(v)对于具有 n 个顶点、e 条边的无向图,无向图的全部顶点的度的和等于边数的 2 倍,因为每条边和两个顶点相关联

        在有向图中顶点 v 的度分为入度出度入度是以顶点 v 为终点的有向边的数目,记为 ID(v)出度是以顶点 v 为起点的有向边的数目,记为 OD(v)

顶点 v 的度等于其入度与出度之和,即 TD(v) = ID(v) + OD(v)对于具有 n 个顶点、e 条边的有向图,有向图的全部顶点的入度之和与出度之和相等,并且等于边数,因为每条有向边都有一个起点和终点。

1.1.10 边的权和网

        在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值这种边上带有权值的图称为带权图,也称为

1.1.11 稠密图、稀疏图

        边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密是相对而言的,稀疏和稠密本身是模糊的概念。一般当 图G 满足 |E| < |V|log|V| 时,可以将 G 视为稀疏图

1.1.12 路径、路径长度和回路

        顶点 V_{p} 到顶点 V_{q} 之间的一条路径是指顶点序列 V_{p} V_{i1} V_{i2}……V_{q},关联的边也可以理解为路径的构成要素。路径上边的数目称为路径长度第一个顶点和最后一个顶点相同的路径称为回路或环若一个图有 n 个顶点,并且有大于 n-1 条边,则此图一定有环

1.1.13 简单路径、简单回路

        在路径序列中,顶点不重复出现的路径称为 简单路径

        除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为 简单回路

1.1.14 距离

        从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离

        若从 u 到 v 根本不存在路径,则记该距离为无穷

1.1.15 有向树

        一个顶点的入度为 0(对应于树的根结点)、其余顶点的入度均为 1 的有向图(对应于根结点下的孩子结点),称为 有向树 

1.2 相关练习

2. 图的存储及基本操作

        图的存储必须要完整、准确地反映顶点集边集的信息。根据不同图的结构和算法,采用不同的存储方式将对程序的效率产生相当大的影响。

2.1 邻接矩阵法

        所谓邻接矩阵存储是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(也就是各顶点之间的邻接关系),存储顶点之间的邻接关系的二维数组称为邻接矩阵

        结点数为 n 的图 G=(V,E) 的邻接矩阵 A 是 n*n 的将 G 的顶点编号为 v_{1}v_{2}……v_{n}。若 (v_{i}v_{j}\varepsilon E,则A[i][j] = 1,否则 A[i][j] = 0;

        对于带权图而言,若顶点 v_{i} 和 v_{j} 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值;若顶点 v_{i} 和 v_{j} 之间不相连,则用 无穷 来表示这两个顶点之间不存在边:

有向图、无向图和网(网就是每条边上都标有权值的图)对应的邻接矩阵如下图所示:

//图的邻接矩阵存储结构定义

#define MaxVertexNum  100    //顶点数目的最大值
typedef char VertexType;   //顶点的数据类型
typedef int EdgeType;    //带权图中边上权值的数据类型
typedef struct
{
    VertexType Vex[MaxVertexNum];  //顶点表
    EdgeType Edge[MaxVertexNum][MaxVertexNum];   //邻接矩阵,边表
    int vexnum,arcnum;  //图的当前顶点数和弧数
}MGraph;

注意:

        ①:在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息均可以省略)

        ②:当邻接矩阵的元素仅表示相应边是否存在时,EdgeType 可采用值为 0 和 1 的枚举类型

        ③:无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储

        ④:邻接矩阵表示法的空间复杂度为 O(n^{2}) ,其中 n 为图的顶点数 |V| 

图的邻接矩阵存储表示法具有以下特点:

        ①:无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需要存储上(或下)三角矩阵的元素

        ②:对于无向图,邻接矩阵的第 i 行(或第 i 列)非零元素(或非无穷元素)的个数正好是顶点 i 的度 TD(v_{i})

        ③:对于有向图,邻接矩阵的第 i 行非零元素(或非无穷元素)的个数正好是顶点 i 的出度 OD(v_{i});第 i 列非零元素(或非无穷元素)的个数正好是顶点 i 的入度 ID(v_{i})

        ④:用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大

        ⑤:稠密图适合用邻接矩阵的存储表示

        ⑥:设图 G 的邻接矩阵为 A,A^{n} 的元素  A^{n}[i][j] 等于由顶点 i 到顶点 j 的长度为 n 的路径的数目

2.2 邻接表法

        之所以引入邻接表法,是因为邻接表法在邻接矩阵法的基础上结合了顺序存储和链式存储方法,大大减少了存储空间上的浪费。

        所谓邻接表是指对图 G中的每个结点 v_{i} 建立一个单链表,第 i 个单链表中的结点表示依附于顶点 v_{i} 的边(对于有向图则是以顶点 v_{i} 为尾的弧),这个单链表称为顶点 v_{i} 的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点

        顶点表结点顶点域(data)指向第一条邻接边的指针(firstarc)构成

        边表(邻接表)结点邻接点域(adjvex)指向下一条邻接边的指针域(nextarc)构成

//图的邻接表存储结构

#define MaxVertexNum  100  //图中顶点数目的最大值
typedef struct ArcNode  //边表结点
{
    int adjvex;  //该弧所指向的顶点的位置
    struct ArcNode *next;  //指向下一条弧的指针
    //InfoType info;   //网的边权值
}ArcNode;
typedef struct VNode  //顶点表结点
{
    VertexType data;  //顶点信息
    ArcNode *first;   //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct
{
    AdjList vertices;  //邻接表
    int vexnum,arcnum;  //图的顶点数和弧数
}ALGraph;  //ALGraph是以邻接表存储的图类型

图的邻接表存储方法具有以下特点:

        ①:若 G 为无向图,则所需的存储空间为 O(|V|+2|E|); 若 G 为有向图,则所需的存储空间为 O(|V|+|E|)。前者的倍数 2 是由于无向图中,每条边在邻接表中出现了两次。

        ②:对应稀疏图,采用邻接表表示将极大地节省存储空间。

        ③:在邻接表中,给定一顶点,通过读取它的邻接表,可以很容易的找到它的所有临边。但是在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为 O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到。

        ④:在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。

        ⑤:图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。

2.3 十字链表

        十字链表有向图的一种链式存储结构。 在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。

        弧结点有 5 个域尾域(tailvex)和头域(headvex)分别指示弧尾和弧头这两个顶点在图中的位置;链域(hlink)指向弧头相同的下一条弧;链域(tlink)指向弧尾相同的下一条弧;info 域指向该弧的相关信息。这样一来,弧头相同的弧就在同一链表上,弧尾相同的弧也在同一链表上。

        顶点结点中有 3 个域data 域存放顶点相关的数据信息,如顶点名称;firstin 和 firstout 两个域分别指向以该结点为弧头或弧尾的第一个弧结点。

        在十字链表中,既容易找到 V_{i} 为尾的弧,又容易找到 V_{i} 为头的弧,相对而言是比较容易求得顶点的入度出度;图的十字链表表示是不唯一的;

2.4 邻接多重表

        邻接多重表无向图的另一种链式存储结构

        与十字链表类似,在邻接多重表中每条边用一个结点表示

其中,mark 为标志域,可用于标记该条边是否被搜索过ivex 和 jvex 为该边依附的两个顶点在图中的位置ilink 指向下一条依附于顶点 ivex 的边jlink 指向下一条依附于顶点  jvex 的边info 为指向和边相关的各种信息的指针域

        每个顶点也用一个结点表示

其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。

        在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,因此每个边结点同时链接在两个链表中。

2.5 图的基本操作

Adjacent(G,x,y):判断 图G 是否存在 边(x,y) 或 <x,y>;

Neighbours(G,x):列出 图G 中与结点x 邻接的边;

InsertVertex(G,x):在 图G 中插入顶点x;

DeleteVertex(G,x):在 图G 中删除顶点x;

AddEdge(G,x,y):若 无向边(x,y) 或有向边 <x,y> 不存在,则向图G中添加该边;

RemoveEdge(G,x,y):若 无向边(x,y) 或有向边 <x,y>  存在,则向图G中删除该边;

FirstNeighbour(G,x):求图G中顶点 x 的第一个邻接点,若有则返回顶点号。若 x 没有邻接点或图中不存在X,则返回 -1;

NextNeighbour(G,x,y):假设 图G 中顶点 y 是顶点 x 的一个邻接点,返回除 y 外顶点 x 的下一个邻接点的顶点号,若 y 是 x 的最后一个邻接点,则返回 -1;

Get_edge_value(G,x,y):获取 图G 中边 (x,y) 或 <x,y> 对应的权值;

Set_edge_value(G,x,y,v):设置 图G 中边 (x,y) 或 <x,y> 对应的权值为 v ;

2.6 相关练习

3. 图的遍历

        图的遍历是指从图中某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问且仅访问一次。注意树是一种特殊的图,所以树的遍历实际上也可以视为一种特殊的图的遍历。图的遍历是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

        图的遍历比树的遍历要复杂的多,因为图的任一顶点都可能和其余的顶点相邻接,所以在访问某个顶点后,可能沿着某条路径搜索又回到了该顶点上。为了避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点,为此可以设一个辅助数组 visited[] 标记顶点是否被访问过

        图的遍历算法主要有两种:广度优先搜索深度优先搜索

3.1 广度优先搜索

        广度优先搜索(Breadth-First-Search,BFS)类似于二叉树的层序遍历算法。基本思想是:首先访问起始结点 v,接着由 v 出发,依次访问 v 的各个未访问过的邻接顶点w_{1}w_{2},……w_{i},然后依次访问w_{1}w_{2},……w_{i} 的所以未被访问过的邻接顶点;再从这些访问过的顶点出发,访问他们所有未被访问过的邻接结点,直至图中所有顶点都被访问过为止。若此时图中尚有顶点未被访问,则另选图中一个未曾访问过的顶点作为起始点,重复上述操作,直至图中所有都被访问到为止。

        广度优先搜索遍历图的过程是以 v 为起始点,由近至远依次访问和 v 有路径想通且路径长度为 1,2,……的顶点。广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批结点,并不像深度优先搜索那样有往回退的情况,因此广度优先搜索并不是一个递归算法。

        为了实现逐层的访问,算法必须借助一个辅助队列,以用来记忆正在访问的顶点的下一层顶点。

//广度优先搜索算法的代码

bool visited[MAX_VERTEX_NUM];   //访问标记数组
void BFSTraverse(Graph G) //对 图G 进行广度优先遍历
{
    for(i=0;i<G.vexnum;++i)
    {
        visited[i]=FALSE;  //访问标记数组初始化
    }
    InitQueue(Q); //初始化辅助队列Q
    for(i=0;i<G.vexnum;++i)  //从 0 号顶点开始遍历
    {
        if(!visited[i]) //对每个连通分量调用一次BFS
            BFS(G,i); //vi 未访问过,从 vi 开始BFS
    }
    void BFS(Graph G,int v) //从顶点 v 出发,广度优先遍历图 G
    {
        visit(v); //访问初始顶点 v
        visited[v]=TRUE; //对 v 做已访问标记
        Enqueue(Q,v); //顶点 v 入队列 Q
        while(!isEmpty(Q))
        {
            DeQueue(Q,v); //顶点 v 出队列
            for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) //检测 v 的所有邻接点
            {
                if(!visited[w]) // w 为 v 尚未访问的邻接顶点
                {
                    visit(w); //访问顶点w
                    visited[w]=TRUE;  //对 w 做已访问标记
                    EnQueue(Q,w); //顶点 w 入队列
                }
            }
        }
    }
}

        辅助数组 visited[] 标志顶点是否被访问过,其初始状态为 FALSE。在图的遍历过程中,一旦某个顶点 v_{i} 被访问,就立即置 visited[i] 为TRUE,防止它被多次访问。

如下,通过实例演示广度优先搜索过程:

        假设从 a 结点开始访问,a 先入队。此时队列非空,取出队头元素 a,由于 b c 与 a 邻接并且未被访问过,于是依次访问 b c,并将 b c 依次入队,此时队列非空,取出队头元素 b ,依次访问与 b 邻接且未被访问的顶点 d,e,并且将 d e 入队(这里注意:虽然 a 也和 b 相邻接,但是 a 已经被 visit[] 标记访问,故不再重复访问),此时队列非空,取出队头元素 c,依次访问与 c 邻接且未被访问的顶点 f g,并且将 f g入队。此时队列非空,取出队头元素 d,但与 d 邻接且未被访问的结点为空,故不做任何操作,紧接着取出队头顶点 e ,将 h入队,紧接着访问 f g,与f g 邻接且未被访问的结点为空;遍历的结果为 abcdefgh。 

        从上例中不难看出,图的广度优先搜索的过程与二叉树的层序遍历是完全一致的。

3.1.1 BFS(广度优先搜索)的性能分析

        无论是邻接表还是邻接矩阵的存储方式BFS 算法都需要借助一个辅助队列 Q,n 个顶点均需入队一次,在最坏的情况下,空间复杂度为 O(|V|)

        采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次),故时间复杂度为 O(|V|) ,在搜索任一顶点的邻接点时,每条边至少访问一次,故时间复杂度为 O(|E|),算法总的时间复杂度为 O(|V|+|E|)。

        采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为 O(|V|),故算法总的时间复杂度为 O(|V|^{2})。

3.1.2 BFS 算法求解单源最短路径问题

        若 图G=(V,E)为非带权图,定义从顶点 u 到顶点 v 的最短路径 d(u,v) 为从 u 到 v 的任何路径中最少的边数;若从 u 到 v 没有通路,则 d(u,v) =无穷;

        使用 BFS,我们可以求解一个满足上述定义的 非带权图 的单源最短路径问题,这是由广度优先搜索总是按照距离由近到远来遍历图中每个顶点的性质决定的。

// BFS 算法求解单源最短路径问题的算法

void BFS_MIN_Distance(Graph G,int u)
// d[i] 表示从 u 到 i 结点的最短路径
{
    for(i=0;i<G.vexnum;++i)
    {
        d[i]=无穷;  //初始化路径长度
    }    
    visited[u]=TRUE; //标记起始结点 u
    d[u]=0; //u作为起始节点,最短路径显然为0
    EnQueue(Q,u); //起始结点入队
    while(!isEmpty(Q)) //只要队列非空,就执行 BFS算法 主过程
    {
        DeQueue(Q,u); //队头元素 u 出队
        for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
        //w=FirstNeighbor(G,u) w为结点 u 的第一个邻接结点
        //w>=0;  也就是 u 不是叶子结点,w 后面还有邻接结点
        //w=NextNeighbor(G,u,w),跳过 u 的第一个邻接结点,找到其后序的邻接结点
        {
            if(!visited[w]) //跳过第一个邻接结点后的后序结点 w 尚未被标记 
            {
                visited[w]=TRUE; //访问到 结点 w 后,将结点 w 标记
                d[w]=d[u]+1; //路径长度 +1 
                EnQueue(Q,w); //顶点 w 入队
            }
        }
    }
}

3.1.3 广度优先生成树

        在广度遍历的过程中,我们可以得到一棵遍历树,称为广度优先生成树

需要注意的是:

        邻接矩阵的广度优先生成树是唯一的;(邻接矩阵的存储方式是唯一的)

        邻接表的广度优先生成树是不唯一的;(邻接表的存储方法不唯一)

3.2 深度优先搜索

        与广度优先搜索不同,深度优先搜索(Depth-First-Search,DFS)类似于树的先序遍历它的基本思想如下:首先访问图中某一起始顶点 v,然后由 v 出发,访问与 v 邻接且未被访问的任一顶点 w_{i},再访问与 w_{1} 邻接且未被访问的任一结点 w_{2}……重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止

//深度优先遍历算法

bool visited[MAX_VERTEX_MUM]; // 访问标记数组
void DFSTraverse(Graph G) // 对 图G 进行深度优先遍历
{
    for(v=0;v<G.vexnum;++v)
    {
        visited[v]=FALSE;  // 初始化已访问标记数据
    }
    for(v=0;v<G.vexnum;++v) // 从 v=0 开始遍历
    {
        if(!visited[v]) // 判断,如果这个结点没有被访问过
            DFS(G,v);  // 对这个结点进行深度优先遍历           
    }
    void DFS(Graph G,int v) // 从 顶点 v 出发,深度优先遍历 图G
    {
        visit(v); //访问 顶点 v
        visited[v]=TRUE; // 已经访问完的结点,设置为已访问结点
    }    
    for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
    {
        if(!visited[w]) // w 是 v 尚未标记的邻接结点
        {
            DFS(G,w);  // 对 v 的邻接结点 w 进行深度优先遍历
        }
    }
}

深度优先搜索的过程:

        首先访问 a ,并置 a 访问标记;然后访问与 a 邻接且未被访问的顶点 b;置 b 访问标记;然后访问与 b 邻接且未被访问的结点 d;此时 d 已经没有未被访问的邻接点,返回 d 的上一个结点 b,然后访问与 b 邻接且未被访问的顶点 e,置 e 访问标记;然后访问与 e 邻接且未被访问的顶点 h,置 h 访问标记;……依次类推;直至图中所有顶点都被访问一次。遍历的结果为 abdehcfg

注意:

        图的邻接矩阵表示是唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不同。

        因此,对于同样一个图,基于邻接矩阵的遍历所得到的 DFS 序列和 BFS 序列是唯一的,

                                                基于邻接表的遍历所得到的 DFS序列和 BFS 序列是不唯一的。

3.2.1 DFS(深度优先遍历)算法的性能分析

        DFS 算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为 O(|V|)

        遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于所用的存储结构。

        以邻接矩阵表示时,查找每个结点的邻接点所需的时间为 O(|V|),故总的时间复杂度为 O(|V|^{2})。

        以邻接表表示时,查找所有顶点的邻接点所需的时间为 O(|E|),访问顶点所需的时间为 O(|V|),此时,总的时间复杂度为 O(|V|+|E|)。

3.2.2 深度优先的生成树和生成森林

        与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。当然,这是有条件的,即对连通图调用 DFS 才能产生深度优先生成树,否则产生的将是深度优先生成森林。与 BFS类似,基于邻接表存储的深度优先生成树是不唯一的。

3.3 图的遍历与图的连通性

        图的遍历算法可以用来判断图的连通性。

        对于无向图来说,若无向图是连通的,则从任一结点出发,仅需一次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。

        对于有向图来说,若从初始点到图中的每个顶点都有路径,则能够访问到图中所有的顶点,否则不能访问到所有的顶点。

        故在 BFSTraverse() 或 DFSTraverse() 中添加了第二个 for 循环,再选取初始点,继续进行遍历,以防止一次无法遍历图的所有顶点。

3.4 相关练习

1. 对一个 n 个顶点、e 条边的图采用邻接表表示时,进行 DFS 遍历的时间复杂度为 O(n+e) , 空间复杂度为 O(n) 。进行 BFS 遍历的时间复杂度为 O(n+e) ,空间复杂度为 O(n) 。

2. 对一个 n 个顶点、e 条边的图采用邻接矩阵表示时,进行 DFS 遍历的时间复杂度为 O(n^{2}),进行 BFS 遍历的时间复杂度为 O(n^{2}) 。

4. 图的应用

        图的应用主要包括:最小生成(代价)树、最短路径、拓扑排序和关键路径

4.1 最小生成树

        一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边(生成树是建立在极小连通子图的基础之上的)。对于生成树来说,若砍去它的一条边,则会使生成树变成非连通图若给它增加一条边,则会形成图中的一条回路

        对于一个带权连通无向图 G = (V , E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设 Q 为 G 的所有生成树的集合,若 T为 Q 中边的权值之和最小的那棵生成树,则  T 称为 G 的最小生成树(Minimum-Sanning-Tree,MST)

最小生成树的性质:

        1. 最小生成树不是唯一的,即最小生成树的树形不唯一。Q 中可能有多个最小生成树。当 图G 中各边权值互不相等时,G 的最小生成树是唯一的;若无向连通图 G的边数比顶点数少1,也就是说 G 本身就是一棵树,则 G 的最小生成树就是它本身。

        2. 最小生成树的边的权值之和总是唯一的,虽然最小生成树不唯一,但其对应的边的权值之和总是唯一的,而且是最小的。

        3. 最小生成树的边数为 顶点数 - 1。

//构造最小生成树的算法大多数都利用了最小生成树的下列性质:

假设 G=(V,E) 是一个带权连通无向图,U 是顶点集 V 的一个非空子集。若 (u,v)是一条具有最小权值的边,则必存在一棵包含边 (u,v)的最小生成树。

//通用的最小生成树的算法

GENERIC_MST(G)
{
    T = NULL;
    while T 未形成一棵生成树;
        do 找到一条最小代价边 (u,v) 并且加入 T 后不会产生回路;
            T=T 并 (u,v);       
}
//每次加入一条权值最小的边以逐渐形成一棵生成树

4.1.1 Prim 算法

Prim(普里姆)算法构造最小生成树的过程如下所示:

        初始时从图中任取一个顶点(如顶点1)加入树T,此时树中只含有一个顶点,之后选择一个与当前 T 中顶点集合距离最近的顶点(这里所谓的距离最近也就是权值最小),并将该顶点和相应的边加入 T,每次操作后 T 中的顶点数和边数都增加 1。以此类推,直至图中所有的顶点都并入 T,得到的 T 就是最小生成树。此时 T 中必然有 n-1 条边

//Prim 算法的简单实现:

void Prim(G,T)
{
    T=∅;  //初始化空树
    U={w};  //添加任一顶点w
    while((V-U)!=∅) //若树中不含全部顶点
    {
        设(u,v)权值最小的边;
        T=T∪{(u,v)}; //边归入树
        U=U∪{v};  //顶点归入树
    }
}

Prim 算法的时间复杂度为 O(|V|^{2}),不依赖于 |E|,因此它适用于求解边稠密的图的最小生成树

4.1.2 Kruskal 算法

        Kruskal(克鲁斯卡尔)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法

Kruskal(克鲁斯卡尔)算法构造最小生成树的过程如下图所示:

        初始时为只有 n 个顶点而无边的非连通图 T={V,{}},每个顶点自成一个连通分量(如下图,六个顶点就有六个连通分量),然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在 T 中不同的连通分量上,则将此边加入 T,否则舍弃此边而选择下一条权值最小的边(这句话的意思是说:我们看下图中的 e 和 f,e 中具有两个连通分量,此时权值为5的边有三条,(V_{2},V_{3}),(V_{1}V_{3}),(V_{3}V_{4}),(V_{1}V_{3}),(V_{3}V_{4})这两条边位于同一个连通分量中,所以舍去,只能选择连接 (V_{2}V_{3})。以此类推,直至 T 中所有顶点都在一个连通分量上。

//Kruskal 算法的简单实现

void Kruskal(V,T)
{
    T=V;  //初始化树T,仅含结点
    numS=n; //连通分量数
    while(numS>1) //因为最小生成树最终所以的连通分量是要合成成一个的
    {
        从 E 中取出权值最小的边(v,u);
        if(v 和 u 属于 T 中不同的连通分量)
        {
            T=T∪{(v,u)};  //将此边加入生成树中
            numS--;   //连通分量数减1
        }
    }
}

        根据图的相关性质,若一条边连接了两棵不同树中的顶点,则对这两棵树来说,他必定是连通的,将这两条边加入森林中,完成两棵树的合并,直到整个森林合并成一棵树。

        通常在 Kruskal 算法中,构造 T 的时间复杂度为 O(|E|log|E|)。因此,Kruskal 算法适合于边稀疏而顶点较多的图

4.2 最短路径

        当图是带权图时,把从一个顶点 v_{0} 到图中其余任意一个顶点 v_{i} 的一条路径(可能不只是一条)所经过边上的权值之和,定义为该路径的带权路径长度把带权路径长度最短的那条路径称为最短路径

        求解最短路径的算法通常都依赖于一种性质即两点之间的最短路径也包含了路径上其他顶点间的最短路径

带权有向图 G 的最短路径问题一般可以分为两类:

        一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可以通过经典的 Dijkstra(迪杰斯特拉)算法求解;

        二是求每对顶点间的最短路径,可通过 Floyd(弗洛伊德)算法来求解。

4.2.1 Dijkstra 算法求单源最短路径问题

        Dijkstra 算法设置一个集合 S 记录已求得的最短路径的顶点,初始时把源点 v_{0} 放入 S,集合 S 没并入一个新顶点 v_{i} ,都要修改源点 v_{0} 到集合 V-S 中顶点当前的最短路径长度值。

这里的意思是说:

        每当一个顶点加入 S 后,可能需要修改源点 v_{0} 到集合 V-S 中可达到当前的最短路径长度;

例如:

        源点为 v_{0} ,初始时 S={v_{0}},dist[1]=3,dist[2]=7,当将 v_{1} 并入集合 S 后,dist[2] 需要更新为 4。

        上述 dist[]:记录从源点 v_{0} 到其他各顶点当前的最短路径长度,它的初态为:若从 v_{0} 到 v_{i} 有弧,则 dist[i] 为弧上的权值;否则置 dist[i] 为无穷。

        再引入一个辅助数组 path[]: path[i] 表示从源点到顶点 i 之间的最短路径的前驱结点。在算法结束时,可以根据其值追溯得到源点 v_{0} 到顶点 v_{i} 的最短路径。

        假设从顶点 0 出发,即 v_{0} = 0,集合 S 最初只包含顶点 0,邻接矩阵 arcs 表示带权有向图,arcs[i][j] 表示有向边 <i,j> 的权值,若不存在有向边 <i,j>,则 arcs[i][j] 为 无穷

Dijkstra 算法的步骤如下:

        1. 初始化:集合 S 初始为 {0},dist[] 的初始值 dist[i] = arcs [0][i],i=1,2,……n-1。(也就是定义从结点 0 出发到结点 i 的最短路径长度)

        2. 从顶点集合 V-S 中选出 v_{j} ,满足 dist[j] = Min{dist[i]},v_{j} 就是当前求得的一条从 v_{0} 出发的最短路径的终点,令 S = S ∪ {j}。

        3. 修改从 v_{0} 出发到集合 V-S 上任一顶点 v_{k} 可达的最短路径长度;

        4. 重复 2-3,操作共 n-1 次,直到所有的顶点都包含在 S 中。

例:

        初始化:集合 S 初始为{v_{1}},v_{1} 可达 v_{2} 和 v_{5}v_{1} 不可达 v_{3} 和 v_{4},因此 dist[] 数组各元素的初始值设置为 dist[2]=10,dist[3]=无穷,dist[4]=无穷,dist[5]=5;

        第一轮:选出最小值 dist[5],将顶点 v_{5} 并入集合 S,即此时已经找到了 v_{1} 到 v_{5} 的最短路径。当 v_{5} 加入 S 后,从 v_{1} 到集合 V-S 中可达顶点的最短路径长度可能会产生改变。因此需要更新 dist[] 数组。v_{5} 可达  v_{2},1->5->2的距离8 要比dist[2]=10的距离短,所以更新 dist[2]=8;5->3,更新 dist[3]=14;5->4,更新 dist[4]=7;

        第二轮:选出最小值 dist[4],将顶点 v_{4} 并入集合 S。继续更新 dist[] 数组。v_{4} 不可达 v_{2} ,dist[2]不变;v_{4} 可达 v_{3},1->5->4->3 的距离 13 dist[3] 小,更新 dist[3]=13;

        第三轮:选出最小值 dist[2],将顶点 v_{2} 并入集合 S。继续更新 dist[] 数组。v_{2} 可达 v_{3},1->5->2->3 的距离 9 比 dist[3] 小,更新 dist[3]=9;

        第四轮:选出唯一最小值 dist[3],将顶点 v_{3} 并入集合 S,此时全部顶点已包含在 S 中。

        使用邻接矩阵表示时,时间复杂度为 O(|V|^{2})

        使用带权的邻接表表示时,时间复杂度为 O(|V|^{2})

 注意:

      若边上带有负权值时, Dijkstra 算法并不适用。

4.2.2 Floyd 算法求各顶点之间最短路径问题

        求顶点之间的最短路径问题描述如下:已知一个各边权值均大于 0 的带权有向图,对任意两个顶点 v_{i}≠ v_{j},要求求出 v_{i} 与 v_{j} 之间的最短路径和最短路径长度。

        Floyd 算法的基本思想是:递推产生一个 n 阶方阵序列 A^{(-1)}A^{(0)}……A^{(k)}……A^{(n-1)},其中A^{(k)}[i][j] 表示从顶点 v_{i} 到顶点 v_{j} 的路径长度,k 表示绕行第 k 个顶点的运算步骤。初始时,对于任意两个顶点 v_{i} 和 v_{j},若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在有向边,则以 无穷 作为它们之间的最短路径长度。以后逐步尝试在原路径中加入顶点 k作为中间顶点。若增加中间顶点后,得到的路径比原来的路径长度减少了,则以此新路径代替原路径。

        定义一个 n 阶方阵序列  A^{(-1)}A^{(0)}……A^{(k)}……A^{(n-1)},其中

        A^{(-1)}[i][j] = arcs[i][j]

        A^{(k)}[i][j] = Min{A^{(k-1)}[i][j],A^{(k-1)}[i][k]+A^{(k-1)}[k][j]},k = 0,1,……n-1

式中,A^{(0)}[i][j] 是从顶点 v_{i} 到 v_{j} 、中间顶点是 v_{0} 的最短路径的长度,A^{(k)}[i][j] 是从顶点 v_{i} 到 v_{j} 、中间顶点的序号不大于 k 的最短路径的长度。

        Floyd 算法是一个迭代的过程,每迭代一次,在从 v_{i} 到 v_{j} 的最短路径上就多考虑一个顶点;经过 n 次迭代后,所得到的 A^{(n-1)}[i][j] 就是  v_{i} 到 v_{j} 的最短路径长度,也就是说 A^{(n-1)} 中就保存了任意一对顶点之间的最短路径长度。

例如:

        初始化: 方阵  A^{(-1)}[i][j] = arcs[i][j];

        第一轮:将 v_{0} 作为中间结点,对于所以顶点对{i,j},如果有 A^{(-1)}[i][j] >A^{-1}[i][0]+A^{-1}[0][j],则将 A^{(-1)}[i][j] 更新为A^{-1}[i][0]+A^{-1}[0][j]。有 A^{(-1)}[2][1] >A^{-1}[2][0]+A^{-1}[0][1],更新 A^{(-1)}[2][1] = 11,更新后的方阵标记为 A^{0}

        第二轮:将 v_{1} 作为中间结点,继续检测全部顶点对{i,j}, 有 A^{0}[0][2] >A^{0}[0][1]+A^{0}[1][2] = 10,更新 A^{0}[0][2] = 10,更新后的方阵标记为 A^{1}

        第三轮:将 v_{2} 作为中间结点,继续检测全部顶点对{i,j}, 有 A^{1}[1][0] >A^{1}[1][2]+A^{1}[2][0] = 9,更新 A^{1}[1][0] = 9,更新后的方阵标记为 A^{2}。此时 A^{2} 中保存的就是任意顶点对的最短路径长度。

 这里我是这么理解的:

        在数值分析这门课中,我们学习过迭代计算的原理;初始化时我们写出 G 的邻接矩阵,邻接矩阵中必然存在权值由大到小的边;

        在邻接矩阵的基础之上,进行迭代,优先考虑权值最大的边,在上图中就是 无穷 > 13 > 10 > 6…… ,所以第一次迭代时先处理权值为无穷的边,如果减少这个边的权值,会相对的影响其他边的权值,进行一步一步的迭代;

        迭代我们都清楚:以 v_{0} 作为中间结点,自然要处理初始化的矩阵;以 v_{1} 作为中间结点,自然而然的要处理 A^{(0)} 矩阵,依次类推;

        Floyd 算法的时间复杂度为 O(|V|^{3}) 。

        Floyd 算法允许图中带负权值的边,但不允许有包含带负权值的边组成的回路。

        Floyd 算法同样适用于带权无向图。

4.3 有向无环图描述表达式

        有向无环图:若一个有向图中不存在环,则称为有向无环图,简称 DAG图

        有向无环图是描述含有公共子式的表示式的有效工具。例如表达式

((a+b)*(b*(c+d))+(c+d)*e)*((c+d)*e)

仔细观察上述表达式,可以发现有一些相同的子表达式 (c+d) 和 (c+d)*e,在二叉树中,它们也重复出现。利用有向无环图,则可实现对相同子式的共享,从而节省存储空间。

4.4 拓扑排序

        AOV网:若用 DAG图(有向无环图)表示一个工程,其顶点表示活动,用有向边 <V_{i}V_{j}> 表示活动 V_{i} 必须先于活动 V_{j} 进行的这样一种关系,则将这种有向图称为 顶点表示活动的网络,记为 AOV网。在 AOV网中,活动 V_{i} 是活动 V_{j} 的直接前驱,活动 V_{j} 是活动 V_{i} 的直接后继,这种前驱和后继关系具有传递性,且任何活动 V_{i} 不能以它自己作为自己的前驱或后继。

        拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:

        ① 每个顶点出现且只出现一次。

        ② 若顶点 A 在序列中排在顶点 B 的前面,则在图中不存在从顶点 B 到 顶点 A 的路径。

        或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后面。每个 AOV网 都有一个或多个拓扑排序序列。

        对一个 AOV网进行拓扑排序的算法有很多,下面介绍比较常用的一种方法的步骤:

        ①:从 AOV网 中选择一个没有前驱的顶点并输出。

        ②:从网中删除该顶点和所有以它为起点的有向边。

        ③:重复 ① 和 ② 直到当前的 AOV网 为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。

接下来介绍拓扑排序过程的示例:

        每轮选择一个入度为 0 (入度为0可以理解为这个结点没有前驱结点,或者干脆的理解为没有箭头指向这个结点)的顶点并输出,然后删除该顶点和所有以它为起点的有向边,最后得到拓扑排序的结果为 {1,2,4,3,5}。

//拓扑排序算法的实现

bool TopologicalSort(Graph G)
{
    InitStack(S);  //初始化栈,存储入度为 0 的结点
    for(int i=0;i<G.vexnum;i++)
    {
        if(indegree[i]==0)
            Push(S,i);  //将所有入度为 0 的顶点进栈
    }
    int count=0;  //计数,记录当前已经输出的顶点数
    while(!IsEmpty(S))  //栈不空,则存在入度为 0 的顶点
    {
        Pop(S,i); //栈顶元素出栈
        Print[count++]=i;  //输出顶点 i
        for(p=G.vertices[i].firstarc;p;p=p->nextarc)
        {//将所有 i 指向的顶点的入度减1,并且将入度减为 0 的顶点压入栈 S
            v=p->adjvex;
            if(!(--indegree[v]))
                Push(S,v);  //入度为 0 ,则入栈
        }
        if(count<G.vexnum)
            return false;  //排序失败,有向图中有回路
        else
            return true;  //拓扑排序成功
    }
}

        由于输出每个顶点的同时还要删除以它为起点的边,故采用邻接表存储时拓扑排序的时间复杂度为 O(|V|+|E|),采用邻接矩阵存储时拓扑排序的时间复杂度为 O(|V|^{2})

        对一个 AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序

        ①:从 AOV网中选择一个没有后继(出度为0)的顶点并输出。

        ②:从网中删除该顶点和所有以它为终点的有向边。

        ③:重复 ① 和 ② 直到当前的 AOV 网为空。

用拓扑排序算法处理 AOV网时,应注意以下问题:

        ①:入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。

        ②:若一个顶点有多个直接后继,则拓扑排序的结果通过不唯一;但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。

        ③:由于 AOV网 中各顶点的地位平等,每个顶点编号是人为的,因此可以按拓扑排序的结果重新编号,生成 AOV网 的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一

般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立。

4.5 关键路径

        在带权有向图中,以顶点表示事件以有向边表示活动以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称 AOE网。AOE网 和 AOV网 都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE网 中的边有权值;而 AOV网 中的边无权值,仅表示顶点之间的前后关系

AOE网 具有以下两个性质:

        ①:只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;

        ②:只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生;

        在 AOE网 中仅有一个入度为 0 的顶点,称为开始顶点(源点),它表示整个工程的开始;网中也仅存在一个出度为 0 的顶点,称为结束顶点(汇点),它表示整个工程的结束。

        在 AOE网 中,有些活动是可以并行进行的。从源点汇点的有向路径可能有多条,并且这些路径长度可能不同。完成不同路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动

        完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成,则整个工程的完成时间就会延长。因此,只要找到了关键活动就找到了关键路径也就可以得出最短完成时间

以下介绍寻找关键活动时所用到的几个参量的定义:

1. 事件 v_{k} 的最早发生时间 ve(k)

        它是指从源点 v_{1 } 到顶点 v_{k} 的最长路径长度。事件 v_{k} 的最早发生时间决定了所有从 v_{k} 开始的活动能够开工的最早时间。可用下面的公式来计算:

        ve(源点) = 0;

        ve(k) = Max{ve(j) + Weight(v_{j}v_{k})}; v_{k} 为 v_{j} 的任意后继,Weight(v_{j}v_{k}) 表示 <v_{j}v_{k}> 上的权值

计算 ve() 值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:

        ①:初始时,令 ve[1……n] = 0;

        ②:输出一个入度为 0 的顶点 v_{j} 时,计算它所有的直接后继顶点 v_{k} 的最早发生时间,若 ve[j] + Weight(v_{j}v_{k})>ve[k],则 ve[j] + Weight(v_{j}v_{k}) = ve[k]。依次类推,直至输出所有顶点。

2. 事件 v_{k} 的最迟发生时间 vl(k)

        它是指在不推迟整个工程完成的前提下,即保证它的后继事件 v_{j} 在其最迟发生时间 vl(j)能够发生,该事件的最迟必须发生的时间。可用下面的公式计算:

        vl(汇点) = ve(汇点);

        vl(k) = Min{vl(j) - Weight(v_{k}v_{j})}; v_{k} 为 v_{j} 的任意前驱

计算 vl() 值时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算:

        ①:初始时,令 vl[1……n] = ve[n];

        ②:栈顶顶点 v_{j} 出栈,计算其所有直接前驱顶点 v_{k} 的最迟发生时间,若  vl(j) - Weight(v_{k}v_{j}) <  vl(k),则 vl(k) =  vl(j) - Weight(v_{k}v_{j})。以此类推,直至输出全部栈中结点。

3. 活动 a_{i} 的最早开始时间 e(i)

        它是指该活动弧的起点所代表的事件的最早发生时间。若边 <v_{k}v_{j}> 表示活动 a_{i},则有 e(i) = ve(k)。

4. 活动 a_{i} 的最迟开始时间 l(i)

        它是指该活动弧的终点所表示的事件的最迟发生时间与该活动所需时间之差。若边  <v_{k}v_{j}> 表示活动 a_{i},则有 l(i) = vl(i) - Weight(v_{k}v_{j})。

5. 一个活动 a_{i} 的最迟开始时间 l(i) 和其最早开始时间 e(i) 的差额 d(i) = l(i) - e(i)

        它是指该活动完成的时间余量,也可以说是在不增加完成整个工程所需总时间的情况下,活动 a_{i} 可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称 l(i) - e(i) = 0 即 l(i) = e(i) 的活动 a_{i} 是关键活动。

求关键路径的算法步骤如下:

        1. 从源点出发,令 ve(源点) = 0,按拓扑有序求其余顶点的最早发生时间 ve()。

        2. 从汇点出发,令 vl(汇点) = ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间 vl()。

        3. 根据各顶点的 ve()值求所有弧的最早开始时间 e()。

        4. 根据各顶点的 vl()值求所有弧的最迟开始时间 l()。

        5. 求 AOE网 中所有活动的差额 d(),找出所有 d() = 0 的活动构成关键路径。

例:

        1. 求最早发生时间 ve():初始 ve(1) = 0,在拓扑排序输出顶点的过程中,求得 ve(2) = 3,ve(3) = 2,ve(4) = max{ve(2) + 2,ve(3) + 4} = max{5,6} = 6,ve(5) = 6,ve(6) = max{ve(5) + 1,ve(4) + 2,ve(3) + 3} = max{7,8,5} =8。(最早发生时间简单来说就是按照拓扑排序,ve(2) = 结点1 到 结点2 的权值,ve(3) = 结点1 到 结点3 的权值,ve(4) 有两条路径,1->2->4,1->3->4,取两条路径上权值和的最大值即可;之所以是最大值,是因为一堆事件发生后,结束的标志是最后一个事件结束后,才会认为这一堆事件都结束了,所以要找所有路径中的最大值)

        2. 求最迟发生时间 vl():初始 vl(6) = 8,在逆拓扑排序出栈过程中,求得 vl(5) = 7,vl(4) = 6,vl(3) = min{vl(4) - 4,vl(6) - 3} = min{2,5} = 2,vl(2) = min{vl(5) - 3,vl(4) - 2} = min{4,4} = 4,vl(1) = 0。(最迟发生时间简单来说就是按照逆拓扑排序,vl(5) = 就是初始 vl(6) = 8 减去 对应于 a_{8} 的权值1,最后结果也就是7,vl(3) = 初始的 vl(6)的值分别减去两条路径上的权值和,取最小的作为 vl(3) 的最迟发生时间,8-2-4=2,8-3=5,最小的也就是2,所以 vl(3) =2)

        3. 弧的最早开始时间 e() 等于该弧的起点的顶点的 ve(),具体结果看下表所示。(等于该弧起点的顶点的最早发生时间:很好理解,a_{2} 的最早开始时间就是 边a_{2} 的起点对应的最早发生时间,也就是顶点 v_{1} 的最早发生时间,也就是0;又比方说 a_{6} 的最早开始时间:对应的就是顶点 V_{3} 的最早发生时间,也就是2) 

        4. 弧的最迟发生时间 l() 等于该弧的终点的顶点 vl() 减去该弧持续的时间,具体结果看下表所示。(等于该弧的终点的顶点 vl() 减去该弧持续的时间:举个例子,比方说 a_{3} 的最迟发生时间,等于 a_{3} 弧终点对应的结点的最迟发生时间,也就是 v_{4} 的最迟发生时间 6,拿这个值减去 a_{3} 弧对应的权值,也就是 6 - 2 = 4,所以 a_{3} 的最迟发生时间就是4)

        5.  根据 l(i) - e(i) = 0 的关键活动,得到关键路径为 (v_{1}v_{3}v_{4}v_{6})。(关键活动也就对应表中等于 0 的弧,也就是弧 a_{2}a_{5}a_{7}

        如果是为了最快的求出关键路径,那么通过求最早发生时间就已经可以确定关键路径了。        

对于关键路径,需要注意:

        1. 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能会变成非关键活动。

        2. 网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。

4.6 相关练习

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/541970.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MapReduce【自定义InputFormat】

MapReduce在处理小文件时效率很低&#xff0c;但面对大量的小文件又不可避免&#xff0c;这个时候就需要相应的解决方案。 默认的输入格式为TextInputFormat&#xff0c;对于小文件&#xff0c;它是按照它的父类FileInputFormat的切片机制来切片的&#xff0c;也就是不管一个文…

Java基础学习(17)网络编程

Java基础学习 一、 网络编程1.1 什么是网络编程1.2 常见的软件架构&#xff1a;1.3 网络编程的三要素1.4 IP1.4.1 InetAddress用法 1.5 端口号1.6 协议1.6.1 UDP协议1.6.1.1 UDP的三种通信方式 1.6.2 TCP协议1.6.2.1 TCP底层原理 一、 网络编程 1.1 什么是网络编程 解释&…

C++常用的支持中文的GUI库Qt 6之一:下载、安装与使用

C常用的支持中文的GUI库Qt 6之一&#xff1a;下载、安装与使用 因为Qt发展变化较快&#xff0c;网上许多介绍Qt的下载、安装与使用已过时&#xff0c;初学者常因行不通而受挫&#xff0c;故此发布本文&#xff0c;以Qt 6.2.4开源版在Windows 10安装与使用为例介绍。 C好用的GU…

DM8:达梦数据库宕机版本记录汇总(持续更新中)

DM8:达梦数据库宕机版本记录汇总&#xff08;持续更新中&#xff09; 环境介绍1 SQL引起的数据库故障1.1 SQL引起的数据库故障汇总表1.2 gdb dmserver core 调试得到数据库崩溃时的内存信息1.3 优化参数解决故障 环境介绍 在测试环境或生产环境&#xff0c;特别情况下会遇到数…

go tool pprof 参数 ‘-base‘ 和 ‘-diff_base‘ 之间的区别

go tool pprof 工具是用于分析由 runtime/pprof包 或 net/http/pprof包产生的profile数据&#xff0c;完整的帮助文档在 https://github.com/google/pprof/blob/main/doc/README.md &#xff0c;pprof 工具支持的参数很多&#xff0c;可以用命令 go tool pprof --help来查看全部…

DJ5-4 交换局域网(第一节课)

目录 一、局域网概述 1、LAN 的特点和分类 2、常见的网络拓扑结构 二、计算机与局域网的连接 三、局域网体系结构 四、链路层寻址地址 1、MAC 地址分配 2、MAC 地址识别 五、ARP 地址解析协议 1、ARP 地址解析协议 2、ARP&#xff1a;两个主机位于同一个局域网 3、…

网络计算模式复习(六)

什么是CDN CDN的全称是Content Delivery Network&#xff0c;即内容分发网络。 其目的通过在现有的Internet中增加一层新的网络架构&#xff0c;将网站的内容发布到最接近用户的网络“边缘”&#xff0c;使用户可以就近取得所需的内容&#xff0c;解决Internet网络拥挤的状况&…

socket套接字通信 TCP传输控制协议/IP网络协议 5.18

B/S :浏览器和服务器 C/S :客户机和服务器 网络的体系结构&#xff1a; 网络的层次结构和每层所使用协议的集合 网络采用分层管理的方法&#xff0c;将网络的功能划分为不同的模块 OSI模型&#xff1a; 共7种&#xff1a; 应用层&#xff1a;接收用户的数据&#xff0c;面…

Blender基础技巧小结(三)

本文续签一篇&#xff1a;Blender基础技巧小结&#xff08;二&#xff09;_皮尔斯巴巴罗的博客-CSDN博客 将物体显示为模板&#xff0c;类似Maya Template 四窗口视图 调整3d视图远截面 Blender并不直接支持放样&#xff0c;可以用第三方插件&#xff0c;但效果并不好 基于me…

usb摄像头驱动打印信息

usb摄像头驱动打印信息 文章目录 usb摄像头驱动打印信息 在ubuntu中接入罗技c920摄像头打印的信息如下&#xff1a; [ 100.873222] usb 3-2: new high-speed USB device number 5 using xhci_hcd [ 101.230728] usb 3-2: New USB device found, idVendor046d, idProduct08e5 …

SpringMVC的拦截器(Interceptor)

文章目录 1 拦截器概念2 拦截器入门案例2.1 环境准备2.2 拦截器开发步骤1:创建拦截器类步骤2:配置拦截器类步骤3:SpringMVC添加SpringMvcSupport包扫描步骤4:运行程序测试步骤5:修改拦截器拦截规则步骤6:简化SpringMvcSupport的编写 3 拦截器参数3.1 前置处理方法3.2 后置处理方…

使用frp进行内网穿透(远程连接内网服务器)

文章目录 云服务购买服务器端&#xff08;即我们购买的服务器&#xff09;配置客户端&#xff08;即我们自己的服务器&#xff09;配置使用xshell登录远程服务器在服务器端设置frp开机自启动在客户端设置frp开机自启动 这里主要介绍使用frp工具进行内网穿透&#xff0c;适合的场…

Spring6和SpringBoot3的新特性-你不得不了解的AOT原来这么简单

Spring6.0新特性 一、Spring的发展历史 二、AOT AOT是Spring6.0提供的一个新特性&#xff0c;Ahead of Time 提前编译。 1.AOT概述 1.1 JIT和AOT的关系 1.1.1 JIT JIT(Just-in-time) 动态编译&#xff0c;即时编译&#xff0c;也就是边运行边编译&#xff0c;也就是在程序运…

LangChain与大型语言模型(LLMs)应用基础教程:角色定义

如果您还没有看过我之前写的两篇博客&#xff0c;请先看一下&#xff0c;这样有助于对本文的理解&#xff1a; LangChain与大型语言模型(LLMs)应用基础教程:Prompt模板 LangChain与大型语言模型(LLMs)应用基础教程:信息抽取 LangChain是大型语言模型(LLM)的应用框架,LangCha…

RK3568平台开发系列讲解(LCD篇)快速了解RK LCD的使用

🚀返回专栏总目录 文章目录 一、内核Config配置二、MIPI配置2.1 引脚配置2.2 背光配置2.3 显示时序配置2.3.1 Power on/off sequence2.3.2 Display-Timings三、EDP配置3.1 引脚配置3.2 EDP背光配置沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 本篇章带大家快速了…

数据结构_查找

目录 1. 查找的基本概念 2. 顺序查找和折半查找 2.1 顺序查找 2.1.1 一般线性表的顺序查找 2.1.2 有序表的顺序查找 2.2 折半查找 2.3 分块查找 2.4 相关练习 3. 树型查找 3.1 二叉排序树 3.1.1 二叉排序树的定义 3.1.2 二叉排序树的查找 3.1.3 二叉排序树…

想要一个本地部署的海洋实景三维展示系统吗?

最近几年实景三维非常火&#xff0c;很多人包括博主都想将自己平时干的海洋测绘项目进行实景三维化&#xff0c;这样做的好处就是无论是管理数据还是成果展示都非常方便。我们可能会使用谷歌地图、奥维地图、图新地球等地图服务软件&#xff0c;它们也提供了一些测量、画图功和…

使用Hexo在Github上搭建个人博客

使用Hexo在Github上搭建个人博客 1. 安装Node和git2. 安装Hexo3. Git与Github的准备工作4. 将Hexo部署到Github5. 开始写作 1. 安装Node和git 在Mac上安装Node.js可以使用Homebrew&#xff0c;使用以下命令安装&#xff1a; brew install node使用以下命令安装Git&#xff1a; …

解决 Uncaught TypeError: SpriteCanvasMaterial is not a constructor.

文章目录 前言一、替代语法总结 前言 上周买了本《Three.js开发指南》, 第三版, 里面的语法不太跟趟, 有点旧, 倒也不能全怪作者, three迭代的确很快. 一、替代语法 这几天没事做, 加上前面本来就接触过Three, 很快进展到了第六章. 在推进 利用Canvas贴图给精灵(Sprite)增加样…

研发工程师玩转Kubernetes——启用microk8s的监控面板(dashboard)

安装插件 microk8s enable dashboard 查看dashboard 地址 由于dashboard是在kube-system的namespace中&#xff0c;我们可以使用下面指令查看它服务的地址。 microk8s kubectl get service -n kube-system kubernetes-dashboard 可以得到地址是https://10.152.183.167。 登…