目录
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| 表示 图G 中顶点的个数,E = {(u,v)|u
V,v
V},用 |E| 表示 图G 中边的条数。
注意:
线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集 V 一定非空,但边集 E 可以为空,此时图中只有顶点而没有边。
下面介绍图的基本概念及一些术语:
1.1.1 有向图
若边集 E 是有向边(也称弧)的有限集合时,则图 G 为有向图。弧是顶点的有序对,记为<v,w>,其中v,w是顶点,v 称为弧尾,w 称为弧头,<v,w>称为从 v 到 w 的弧,也称为 v 邻接到 w。
上图中 有向图G1 可以表示为:
= (
,
)
= {1,2,3}
= {<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 可以表示为:
= {
,
}
= {1,2,3,4}
= {(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)}
1.1.3 简单图、多重图
一个图 G 如果满足:
① 不存在重复边;② 不存在顶点到自身的边,那么称 图G 为简单图。 上图中 有向图 和 无向图
均为简单图。
若 图G 中某两个顶点之间的边数大于 1 条,又允许顶点通过一条边和自身关联,则称 图G 为多重图。
1.1.4 完全图(也称简单完全图)
对于无向图,边数 |E| 的取值范围为 0 到 n(n-1)/2(意思是就是任意两个结点之间都成产生一条边,),有 n(n-1)/2 条边的无向图称为 完全图 ,在完全图中任意两个顶点之间都存在边。对于有向图,边数 |E| 的取值范围为 0 到 n(n-1),有 n(n-1) 条弧的有向图称为 有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。无向图
为无向完全图。
1.1.5 子图
设有两个图 G=(V,E) 和 =(
,
),若
是 V 的子集,且
是 E 的子集,则称
是 G 的子图。若有满足 V(
) = V(G) 的子图
,则称其为 G 的生成子图。
注意:
并非 V 和 E 的任何子集都能构成 G 的子图,因为这样的子集可能不是图,即 E 的子集中的某些边关联的顶点可能不在这个 V 的子集中。
1.1.6 连通、连通图和连通分量
在无向图中,若从顶点 v 到顶点 w 的路径存在,则称 v 和 w 是连通的。若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非连通图。无向图中的 极大连通子图 称为连通分量。
如上图中 图6.2 a 为 无向图,图6.2 b 为其三个连通分量。
注:
假设一个图有 n 个顶点,如果边数小于 n-1,那么此图必然是非连通图。
对于 n 个顶点的无向图 G:
若 G 是连通图,则最少有 n-1 条边
若 G 是非连通图,则最多可能有
条边
对于 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 路径、路径长度和回路
顶点 到顶点
之间的一条路径是指顶点序列
……
,关联的边也可以理解为路径的构成要素。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有 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 的顶点编号为 ,
……
。若 (
,
)
E,则A[i][j] = 1,否则 A[i][j] = 0;
对于带权图而言,若顶点 和
之间有边相连,则邻接矩阵中对应项存放着该边对应的权值;若顶点
和
之间不相连,则用 无穷 来表示这两个顶点之间不存在边:
有向图、无向图和网(网就是每条边上都标有权值的图)对应的邻接矩阵如下图所示:
//图的邻接矩阵存储结构定义
#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 为图的顶点数 |V|
图的邻接矩阵存储表示法具有以下特点:
①:无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需要存储上(或下)三角矩阵的元素
②:对于无向图,邻接矩阵的第 i 行(或第 i 列)非零元素(或非无穷元素)的个数正好是顶点 i 的度 TD()
③:对于有向图,邻接矩阵的第 i 行非零元素(或非无穷元素)的个数正好是顶点 i 的出度 OD();第 i 列非零元素(或非无穷元素)的个数正好是顶点 i 的入度 ID(
)
④:用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大
⑤:稠密图适合用邻接矩阵的存储表示
⑥:设图 G 的邻接矩阵为 A, 的元素
[i][j] 等于由顶点 i 到顶点 j 的长度为 n 的路径的数目
2.2 邻接表法
之所以引入邻接表法,是因为邻接表法在邻接矩阵法的基础上结合了顺序存储和链式存储方法,大大减少了存储空间上的浪费。
所谓邻接表,是指对图 G中的每个结点 建立一个单链表,第 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 两个域分别指向以该结点为弧头或弧尾的第一个弧结点。
在十字链表中,既容易找到 为尾的弧,又容易找到
为头的弧,相对而言是比较容易求得顶点的入度和出度的;图的十字链表表示是不唯一的;
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 的各个未访问过的邻接顶点,
,……
,然后依次访问
,
,……
的所以未被访问过的邻接顶点;再从这些访问过的顶点出发,访问他们所有未被访问过的邻接结点,直至图中所有顶点都被访问过为止。若此时图中尚有顶点未被访问,则另选图中一个未曾访问过的顶点作为起始点,重复上述操作,直至图中所有都被访问到为止。
广度优先搜索遍历图的过程是以 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。在图的遍历过程中,一旦某个顶点 被访问,就立即置 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()。
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 邻接且未被访问的任一顶点 ,再访问与
邻接且未被访问的任一结点
……重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
//深度优先遍历算法
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()。
以邻接表表示时,查找所有顶点的邻接点所需的时间为 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(),进行 BFS 遍历的时间复杂度为 O(
) 。
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(),不依赖于 |E|,因此它适用于求解边稠密的图的最小生成树。
4.1.2 Kruskal 算法
Kruskal(克鲁斯卡尔)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。
Kruskal(克鲁斯卡尔)算法构造最小生成树的过程如下图所示:
初始时为只有 n 个顶点而无边的非连通图 T={V,{}},每个顶点自成一个连通分量(如下图,六个顶点就有六个连通分量),然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在 T 中不同的连通分量上,则将此边加入 T,否则舍弃此边而选择下一条权值最小的边(这句话的意思是说:我们看下图中的 e 和 f,e 中具有两个连通分量,此时权值为5的边有三条,(,
),(
,
),(
,
),(
,
),(
,
)这两条边位于同一个连通分量中,所以舍去,只能选择连接 (
,
))。以此类推,直至 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 最短路径
当图是带权图时,把从一个顶点 到图中其余任意一个顶点
的一条路径(可能不只是一条)所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径。
求解最短路径的算法通常都依赖于一种性质,即两点之间的最短路径也包含了路径上其他顶点间的最短路径。
带权有向图 G 的最短路径问题一般可以分为两类:
一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可以通过经典的 Dijkstra(迪杰斯特拉)算法求解;
二是求每对顶点间的最短路径,可通过 Floyd(弗洛伊德)算法来求解。
4.2.1 Dijkstra 算法求单源最短路径问题
Dijkstra 算法设置一个集合 S 记录已求得的最短路径的顶点,初始时把源点 放入 S,集合 S 没并入一个新顶点
,都要修改源点
到集合 V-S 中顶点当前的最短路径长度值。
这里的意思是说:
每当一个顶点加入 S 后,可能需要修改源点
到集合 V-S 中可达到当前的最短路径长度;
例如:
源点为
,初始时 S={
},dist[1]=3,dist[2]=7,当将
并入集合 S 后,dist[2] 需要更新为 4。
上述 dist[]:记录从源点 到其他各顶点当前的最短路径长度,它的初态为:若从
到
有弧,则 dist[i] 为弧上的权值;否则置 dist[i] 为无穷。
再引入一个辅助数组 path[]: path[i] 表示从源点到顶点 i 之间的最短路径的前驱结点。在算法结束时,可以根据其值追溯得到源点 到顶点
的最短路径。
假设从顶点 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 中选出 ,满足 dist[j] = Min{dist[i]},
就是当前求得的一条从
出发的最短路径的终点,令 S = S ∪ {j}。
3. 修改从 出发到集合 V-S 上任一顶点
可达的最短路径长度;
4. 重复 2-3,操作共 n-1 次,直到所有的顶点都包含在 S 中。
例:
初始化:集合 S 初始为{
},
可达
和
,
不可达
和
,因此 dist[] 数组各元素的初始值设置为 dist[2]=10,dist[3]=无穷,dist[4]=无穷,dist[5]=5;
第一轮:选出最小值 dist[5],将顶点
并入集合 S,即此时已经找到了
到
的最短路径。当
加入 S 后,从
到集合 V-S 中可达顶点的最短路径长度可能会产生改变。因此需要更新 dist[] 数组。
可达
,1->5->2的距离8 要比dist[2]=10的距离短,所以更新 dist[2]=8;5->3,更新 dist[3]=14;5->4,更新 dist[4]=7;
第二轮:选出最小值 dist[4],将顶点
并入集合 S。继续更新 dist[] 数组。
不可达
,dist[2]不变;
可达
,1->5->4->3 的距离 13 dist[3] 小,更新 dist[3]=13;
第三轮:选出最小值 dist[2],将顶点
并入集合 S。继续更新 dist[] 数组。
可达
,1->5->2->3 的距离 9 比 dist[3] 小,更新 dist[3]=9;
第四轮:选出唯一最小值 dist[3],将顶点
并入集合 S,此时全部顶点已包含在 S 中。
使用邻接矩阵表示时,时间复杂度为 O()。
使用带权的邻接表表示时,时间复杂度为 O()。
注意:
若边上带有负权值时, Dijkstra 算法并不适用。
4.2.2 Floyd 算法求各顶点之间最短路径问题
求顶点之间的最短路径问题描述如下:已知一个各边权值均大于 0 的带权有向图,对任意两个顶点 ≠
,要求求出
与
之间的最短路径和最短路径长度。
Floyd 算法的基本思想是:递推产生一个 n 阶方阵序列 ,
……
……
,其中
[i][j] 表示从顶点
到顶点
的路径长度,k 表示绕行第 k 个顶点的运算步骤。初始时,对于任意两个顶点
和
,若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在有向边,则以 无穷 作为它们之间的最短路径长度。以后逐步尝试在原路径中加入顶点 k作为中间顶点。若增加中间顶点后,得到的路径比原来的路径长度减少了,则以此新路径代替原路径。
定义一个 n 阶方阵序列 ,
……
……
,其中
[i][j] = arcs[i][j]
[i][j] = Min{
[i][j],
[i][k]+
[k][j]},k = 0,1,……n-1
式中,[i][j] 是从顶点
到
、中间顶点是
的最短路径的长度,
[i][j] 是从顶点
到
、中间顶点的序号不大于 k 的最短路径的长度。
Floyd 算法是一个迭代的过程,每迭代一次,在从 到
的最短路径上就多考虑一个顶点;经过 n 次迭代后,所得到的
[i][j] 就是
到
的最短路径长度,也就是说
中就保存了任意一对顶点之间的最短路径长度。
例如:
初始化: 方阵
[i][j] = arcs[i][j];
第一轮:将
作为中间结点,对于所以顶点对{i,j},如果有
[i][j] >
[i][0]+
[0][j],则将
[i][j] 更新为
[i][0]+
[0][j]。有
[2][1] >
[2][0]+
[0][1],更新
[2][1] = 11,更新后的方阵标记为
。
第二轮:将
作为中间结点,继续检测全部顶点对{i,j}, 有
[0][2] >
[0][1]+
[1][2] = 10,更新
[0][2] = 10,更新后的方阵标记为
。
第三轮:将
作为中间结点,继续检测全部顶点对{i,j}, 有
[1][0] >
[1][2]+
[2][0] = 9,更新
[1][0] = 9,更新后的方阵标记为
。此时
中保存的就是任意顶点对的最短路径长度。
这里我是这么理解的:
在数值分析这门课中,我们学习过迭代计算的原理;初始化时我们写出 G 的邻接矩阵,邻接矩阵中必然存在权值由大到小的边;
在邻接矩阵的基础之上,进行迭代,优先考虑权值最大的边,在上图中就是 无穷 > 13 > 10 > 6…… ,所以第一次迭代时先处理权值为无穷的边,如果减少这个边的权值,会相对的影响其他边的权值,进行一步一步的迭代;
迭代我们都清楚:以
作为中间结点,自然要处理初始化的矩阵;以
作为中间结点,自然而然的要处理
矩阵,依次类推;
Floyd 算法的时间复杂度为 O() 。
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图(有向无环图)表示一个工程,其顶点表示活动,用有向边 <,
> 表示活动
必须先于活动
进行的这样一种关系,则将这种有向图称为 顶点表示活动的网络,记为 AOV网。在 AOV网中,活动
是活动
的直接前驱,活动
是活动
的直接后继,这种前驱和后继关系具有传递性,且任何活动
不能以它自己作为自己的前驱或后继。
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
① 每个顶点出现且只出现一次。
② 若顶点 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()。
对一个 AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
①:从 AOV网中选择一个没有后继(出度为0)的顶点并输出。
②:从网中删除该顶点和所有以它为终点的有向边。
③:重复 ① 和 ② 直到当前的 AOV 网为空。
用拓扑排序算法处理 AOV网时,应注意以下问题:
①:入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。
②:若一个顶点有多个直接后继,则拓扑排序的结果通过不唯一;但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。
③:由于 AOV网 中各顶点的地位平等,每个顶点编号是人为的,因此可以按拓扑排序的结果重新编号,生成 AOV网 的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一
般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立。
4.5 关键路径
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称 AOE网。AOE网 和 AOV网 都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE网 中的边有权值;而 AOV网 中的边无权值,仅表示顶点之间的前后关系。
AOE网 具有以下两个性质:
①:只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
②:只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生;
在 AOE网 中仅有一个入度为 0 的顶点,称为开始顶点(源点),它表示整个工程的开始;网中也仅存在一个出度为 0 的顶点,称为结束顶点(汇点),它表示整个工程的结束。
在 AOE网 中,有些活动是可以并行进行的。从源点到汇点的有向路径可能有多条,并且这些路径长度可能不同。完成不同路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成,则整个工程的完成时间就会延长。因此,只要找到了关键活动,就找到了关键路径,也就可以得出最短完成时间。
以下介绍寻找关键活动时所用到的几个参量的定义:
1. 事件 的最早发生时间 ve(k)
它是指从源点 到顶点
的最长路径长度。事件
的最早发生时间决定了所有从
开始的活动能够开工的最早时间。可用下面的公式来计算:
ve(源点) = 0;
ve(k) = Max{ve(j) + Weight(,
)};
为
的任意后继,Weight(
,
) 表示 <
,
> 上的权值
计算 ve() 值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:
①:初始时,令 ve[1……n] = 0;
②:输出一个入度为 0 的顶点
时,计算它所有的直接后继顶点
的最早发生时间,若 ve[j] + Weight(
,
)>ve[k],则 ve[j] + Weight(
,
) = ve[k]。依次类推,直至输出所有顶点。
2. 事件 的最迟发生时间 vl(k)
它是指在不推迟整个工程完成的前提下,即保证它的后继事件 在其最迟发生时间 vl(j)能够发生,该事件的最迟必须发生的时间。可用下面的公式计算:
vl(汇点) = ve(汇点);
vl(k) = Min{vl(j) - Weight(,
)};
为
的任意前驱
计算 vl() 值时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算:
①:初始时,令 vl[1……n] = ve[n];
②:栈顶顶点
出栈,计算其所有直接前驱顶点
的最迟发生时间,若 vl(j) - Weight(
,
) < vl(k),则 vl(k) = vl(j) - Weight(
,
)。以此类推,直至输出全部栈中结点。
3. 活动 的最早开始时间 e(i)
它是指该活动弧的起点所代表的事件的最早发生时间。若边 <,
> 表示活动
,则有 e(i) = ve(k)。
4. 活动 的最迟开始时间 l(i)
它是指该活动弧的终点所表示的事件的最迟发生时间与该活动所需时间之差。若边 <,
> 表示活动
,则有 l(i) = vl(i) - Weight(
,
)。
5. 一个活动 的最迟开始时间 l(i) 和其最早开始时间 e(i) 的差额 d(i) = l(i) - e(i)
它是指该活动完成的时间余量,也可以说是在不增加完成整个工程所需总时间的情况下,活动 可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称 l(i) - e(i) = 0 即 l(i) = e(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 减去 对应于
的权值1,最后结果也就是7,vl(3) = 初始的 vl(6)的值分别减去两条路径上的权值和,取最小的作为 vl(3) 的最迟发生时间,8-2-4=2,8-3=5,最小的也就是2,所以 vl(3) =2)
3. 弧的最早开始时间 e() 等于该弧的起点的顶点的 ve(),具体结果看下表所示。(等于该弧起点的顶点的最早发生时间:很好理解,
的最早开始时间就是 边
的起点对应的最早发生时间,也就是顶点
的最早发生时间,也就是0;又比方说
的最早开始时间:对应的就是顶点
的最早发生时间,也就是2)
4. 弧的最迟发生时间 l() 等于该弧的终点的顶点 vl() 减去该弧持续的时间,具体结果看下表所示。(等于该弧的终点的顶点 vl() 减去该弧持续的时间:举个例子,比方说
的最迟发生时间,等于
弧终点对应的结点的最迟发生时间,也就是
的最迟发生时间 6,拿这个值减去
弧对应的权值,也就是 6 - 2 = 4,所以
的最迟发生时间就是4)
5. 根据 l(i) - e(i) = 0 的关键活动,得到关键路径为 (
,
,
,
)。(关键活动也就对应表中等于 0 的弧,也就是弧
,
,
)
如果是为了最快的求出关键路径,那么通过求最早发生时间就已经可以确定关键路径了。
对于关键路径,需要注意:
1. 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能会变成非关键活动。
2. 网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。