参考王道《数据结构》的大体知识结构
个人代码存放 github:数据结构
个人博客(由于用阿里免费的DNS解析,所以很大概率会无法访问,建议第一次访问后等几分钟):stardust的博客
仅记录个人学习情况
语言使用c/c++、java
主要目的是补充知识体系,顺便解释一下一些未说明清楚的地方,以及使用java语言来实现数据结构,如有侵权的行为会进行删除
禁转
持续更新中…
第六章 图
6.1图的基本概念
6.1.1图的定义
图 G G G由顶点集 V V V和边集 E E E组成,记为 G = ( V , E ) G=(V,E) G=(V,E),其中 V ( G ) V(G) V(G)表示图 G G G中顶点的有限非续集; E ( G ) E(G) E(G)表示图 G G G中顶点之间的关系(边)集合。若 V = { v 1 , v 2 , ⋯ , v n } V=\{v_1,v_2,\cdots,v_n\} V={v1,v2,⋯,vn},则用 ∣ V ∣ |V| ∣V∣表示图 G G G中顶点的个数, E = { ( u , v ) ∣ u ∈ V , v ∈ V } E=\{(u,v)\mid u\in V,v\in V\} E={(u,v)∣u∈V,v∈V},用 ∣ E ∣ |E| ∣E∣表示图 G G G中边的条数。
线性表可以是空表,树可以是空树,但图不可以是空图。也就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。
下面是图的一些基本概念及术语。
1.有向图
若 E E E是有向边(也称弧)的有限集合,则图 G G G为有向图。弧是顶点的有序对,记为 < v , w > <v,w> <v,w>,其中 v , w v,w v,w是顶点, v v v称为弧尾, w w w称为孤头, < v , w > <v,w> <v,w>称为从 v v v到 w w w的弧,也称 v v v邻接到 w w w。
图(a)所示的有向图
G
1
G_{1}
G1可表示为
G
1
=
(
V
1
,
E
1
)
V
1
=
{
1
,
2
,
3
}
E
1
=
{
<
1
,
2
>
,
<
2
,
1
>
,
<
2
,
3
>
}
G_1=(V_1,E_1)\\V_{1}=\{1,2,3\}\\E_{1}=\{<1,2>,<2,1>,<2,3>\}
G1=(V1,E1)V1={1,2,3}E1={<1,2>,<2,1>,<2,3>}
2.无向图
若 E E E 是无向边(简称边)的有限集合,则图 G G G 为无向图。边是顶点的无序对,记为 ( v , w ) (v,w) (v,w)或 ( w , v ) (w,v) (w,v)。可以说 w w w 和 v v v 互为邻接点。边 ( v , w ) (v,w) (v,w)依附于 w w w 和 v v v,或称边 ( v , w ) (v,w) (v,w)和 v , w v,w v,w 相关联。
图(b)所示的无向图
G
2
G_{2}
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
)
}
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)\}
G2=(V2,E2)V2={1,2,3,4}E2={(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)}
3.简单图、多重图
一个图 G G G 若满足:1.不存在重复边;2.不存在顶点到自身的边,则称图 G G G 为简单图。上图中 G 1 G_{1} G1和 G 2 G_{2} G2均为简单图。若图 G G G 中某两个顶点之间的边数大于 1 条,又允许顶点通过一条边和自身关联,则称图 G G G为多重图。多重图和简单图的定义是相对的。本书中仅讨论简单图。
4.完全图(也称简单完全图)
对于无向图,|E|的取值范围为0到n(n-1)/2,有n(n-1)/2条边的无向图称为完全图,在完全图中任意两个顶点之间都存在边。对于有向图,|E|的取值范围为0到n(n-1),有n(n-1)条弧的有向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。上图中 G 2 G_{2} G2为无向完全图,而 G 3 G_{3} G3为有向完全图。
5.子图
设有两个图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)和
G
′
=
(
V
′
,
E
′
)
G^\prime=(V',E')
G′=(V′,E′),若
V
′
V'
V′是
V
V
V的子集,且
E
′
E'
E′是
E
E
E的子集,则称
G
′
G^\prime
G′是
G
G
G的子图。若有满足
V
(
G
′
)
=
V
(
G
)
V(G^\prime)=V(G)
V(G′)=V(G)的子图
G
′
G^\prime
G′, 则称其为
G
G
G 的生成子图
。例图中
G
3
G_3
G3为
G
1
G_1
G1的子图。
并非 V V V和 E E E的任何子集都能构成 G G G 的子图,因为这样的子集可能不是图,即 E E E的子集中的某些边关联的顶点可能不在这个 V V V的子集中。
6.连通、连通图、连通分量
图的连通性与边和顶点的关系
在无向图中,若从顶点
ν
\nu
ν到顶点
w
w
w有路径存在,则称
ν
\nu
ν和
w
w
w是连通的。若图
G
G
G中任意两个顶点都是连通的,则称图
G
G
G为连通图,否则称为非连通图。无向图中的极大连通子图(这个连通图本身,是唯一的)称为连通分量
,在图(a)中,图
G
4
G_{4}
G4有 3 个连通分量如图(b)所示。假设一个图有
n
n
n个顶点,若边数小于
n
−
1
n-1
n−1,则此图必是非连通图;思考,若图是非连通图,则最多可以有多少条边?
7.强连通图、强连通分量
在有向图中,若有一对顶点
ν
\nu
ν和
w
w
w,从
v
v
v到
w
w
w和从
w
w
w到
v
v
v之间都有路径,则称这两个顶点是强连通
的。若图中任意一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量
,图
G
1
G_1
G1的强连通分量如图所示。思考,假设一个有向图有
n
n
n 个顶点,若是强连通图,则最少需要有多少条边?
在无向图中讨论连通性,在有向图中讨论强连通性。
8.生成树、生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图(删除任意一个边都不再是连通图)
。若图中顶点数为
n
n
n,则它的生成树含有
n
−
1
n-1
n−1条边。包含图中全部顶点的极小连通子图,只有生成树满足这个极小条件,对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。在非连通图中,连通分量的生成树构成了非连通图的生成森林。图
G
2
G_{2}
G2的一个生成树如图所示。
区分极大连通子图和极小连通子图。极大连通子图要求子图必须连通,而且包含尽可能多的顶点和边;极小连通子图是既要保持子图连通又要使得边数最少的子图。
9.顶点的度、入度和出度
无向图中顶点和边的关系
在无向图中,顶点 v v v的度是指依附于顶点 v v v的边的条数,记为 TD(v)。无向图的全部顶点的度之和等于边数的2倍,因为每条边和两个顶点相关联。
在有向图中,顶点 ν \nu ν的度分为入度和出度,入度是以顶点 v v v为终点的有向边的数目,记为ID(v); 而出度是以顶点 ν \nu ν为起点的有向边的数目,记为 OD(v)。顶点 v v v的度等于其入度与出度之和,即 TD ( v ) = ( v ) = (v)=ID ( v ) + ( v ) + (v)+OD ( v ) ( v ) (v)。有向图的全部顶点的入度之和与出度之和相等,并且等于边数,这是因为每条有向边都有一个起点和终点。
10.边的权和网
边的权和网在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网
11.稠密图、稀疏图
边数很少的图称为稀疏图。反之称为稠密图。稀疏和稒密本身是模糊的概念。稀疏图和稠密图常常是相对而言的。一般当图 G G G 满足 ∣ E ∣ < ∣ V ∣ | E| < | V| ∣E∣<∣V∣log ∣ V ∣ | V| ∣V∣时,可以将 G G G 视为稀疏图
12.路径、路径长度和回路
顶点到顶点之间的一条路径是指顶点序列 v p , v i 1 , v i 2 , . . . , v i m , v q v_p,v_{i_1},v_{i_2},...,v_{i_m},v_q vp,vi1,vi2,...,vim,vq,当然关联的边也可理解为路径的构成要素。路径上的边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n个顶点,且有大于n-1条边,则此图一定有环。
13.简单路径、简单回路
路径、回路、简单路径、简单回路的定义
在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
14.距离
从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷( ∞ \infty ∞)。
15.有向树
一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
6.2图的存储及基本操作
图的存储必须要完整、准确地反映顶点集和边集的信息。根据不同图的结构和算法,采用不同的存储方式将对程序的效率产生相当大的影响,因此所选的存储结构应适合于待求解的问题。
6.2.1邻接矩阵法
所谓邻接矩阵存储,是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各项点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。
顶点数为
n
n
n的图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)的邻接矩阵
A
A
A是
n
×
n
n\times n
n×n的,将
G
G
G的顶点编号为
v
1
,
v
2
,
⋯
,
v
n
v_1,v_{2},\cdots,v_{n}
v1,v2,⋯,vn,则
A
[
i
]
[
j
]
=
{
1
,
(
v
i
,
v
j
)
或
⟨
v
i
,
v
j
⟩
是
E
(
G
)
中的边
0
,
(
v
i
,
v
j
)
或
⟨
v
i
,
v
j
⟩
不是
E
(
G
)
中的边
A[i][j]=\begin{cases}1,&(v_i,v_j)\text{或}\langle v_i,v_j\:\rangle\text{是}E(G)\text{中的边}\\0,&(v_i,v_j)\text{或}\langle v_i,v_j\:\rangle\text{不是}E(G)\text{中的边}\end{cases}
A[i][j]={1,0,(vi,vj)或⟨vi,vj⟩是E(G)中的边(vi,vj)或⟨vi,vj⟩不是E(G)中的边
图的邻接矩阵存储及相互转换
对带权图而言,若顶点
v
i
v_i
vi和
v
j
v_j
vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点
V
i
V_i
Vi和
V
j
V_j
Vj不相连,则通常用 0 或
∞
\infty
∞来代表这两个顶点之间不存在边:
A
[
i
]
[
j
]
=
{
w
i
j
,
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
是
E
(
G
)
中的边
0
或
∞
,
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
不是
E
(
G
)
中的边
A[i][j]=\begin{cases}w_{ij},&(v_i,v_j)\text{或}<v_i,v_j>\text{是}E(G)\text{中的边}\\0\text{或}\infty,&(v_i,v_j)\text{或}<v_i,v_j>\text{不是}E(G)\text{中的边}\end{cases}
A[i][j]={wij,0或∞,(vi,vj)或<vi,vj>是E(G)中的边(vi,vj)或<vi,vj>不是E(G)中的边
有向图、无向图和网对应的邻接矩阵示例如图所示。
(算法题)邻接矩阵的遍历及顶点的度的计算
图的邻接矩阵存储结构定义如下:
#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 ) O(n^2) O(n2),其中 n n n为图的顶点数 ∣ V ∣ o |V|_{\mathrm{o}} ∣V∣o
邻接矩阵的遍历的时间复杂度
图的邻接矩阵存储表示法具有以下特点:
- 无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
基于邻接矩阵的顶点的度的计算
- 对于无向图,邻接矩阵的第 i i i行(或第 i i i列)非零元素(或非 ∞ \infty ∞元素)的个数正好是顶点 i i i的度 TD ( v i ) (v_i) (vi)。
- 对于有向图,邻接矩阵的第 i i i 行非零元素(或非 ∞ \infty ∞元素)的个数正好是顶点 i i i 的出度OD ( v i ) ; (v_i); (vi);第 i i i列非零元素(或非 ∞ \infty ∞元素)的个数正好是顶点 i i i的入度 ID ( v i ) (v_i) (vi)。
- 用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
- 稠密图 (即边数较多的图)适合采用邻接矩阵的存储表示。
计算 A 2 A^2 A2并说明 A n [ i ] [ j ] A^n[i][j] An[i][j]的含义 - 设图 G G G的邻接矩阵为 A , A n A,A^n A,An的元素 A n [ i ] [ j ] A^n[i][j] An[i][j]等于由顶点 i i i到顶点 j j j的长度为 n n n的路径的数目。该结论了解即可,证明方法可参考离散数学教材。
6.2.2邻接表法
当一个图为稀疏图时,使用邻接矩阵法显然会浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
所谓邻接表,是指对图 G G G中的每个顶点 v i v_i vi建立一个单链表,第 i i i个单链表中的结点表示依附于顶点 v i v_i vi的边(对于有向图则是以顶点 v i v_i vi为尾的弧),这个单链表就称为顶点 v i v_i vi的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储,称为顶点表,所以在邻接表中存在两种结点:顶点表结点和边表结点,如图所示。
顶点表结点由两个域组成:顶点域(data)存储顶点 v i v_i vi的相关信息,边表头指针域(firstarc)指向第一条边的边表结点。边表结点至少由两个域组成:邻接点域(adjvex)存储与头结点顶点 v i v_i vi邻接的顶点编号,指针域(nextarc)指向下一条边的边表结点。
图的邻接表存储的应用
无向图和有向图的邻接表的实例分别如图所示。
图的邻接表存储结构定义如下:
#define MaxVertexNum 100
//边表结点
typedef struct ArcNode{
//弧指向的结点
int adjvex;
//指向下一条弧的指针
struct ArcNode *nextarc;
//InfoType info;//网的边权值
}ArcNode;
//顶点表结点
typedef struct VNode{
//顶点信息
VertexType data;
//指向第一条依附该顶点的弧的指针
ArcNode *firstarc;
}Vnode,AdjList[MaxVertexNum];
typedef struct{
//邻接表
AdjList vertices;
//顶点数和弧数
int vexnum,arcnum;
}ALGraph;//以邻接表存储的图的类型
图的邻接表存储方法具有以下特点:
- 若 G G G为无向图,则所需的存储空间为 O ( ∣ V ∣ + 2 ∣ E ∣ ) ; O( | V| + 2| E| ) ; O(∣V∣+2∣E∣); 若 G G G为有向图,则所需的存储空间为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。前者的倍数 2 是因为在无向图中,每条边在邻接表中出现了两次。
邻接矩阵法和邻接表法的适用性差异
- 对于稀疏图(即边数较少的图),采用邻接表表示将极大地节省存储空间。
- 在邻接表中,给定一个顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为 O ( n ) O(n) O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
- 在无向图的邻接表中,求某个顶点的度只需计算其邻接表中的边表结点个数。在有向图的邻接表中,求某个顶点的出度只需计算其邻接表中的边表结点个数;但求某个顶点 x x x的入度则需遍历全部的邻接表,统计邻接点(adjvex)域为 x x x的边表结点个数。
- 图的邻接表表示并不唯一,因为在每个顶点对应的边表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。
6.2.3十字链表
十字链表是有向图
的一种链式存储结构。在十字链表中,有向图的每条弧用一个结点(弧结点)来表示,每个顶点也用一个结点(顶点结点)来表示。两种结点的结构如下所示。
弧结点 | tailvex | headvex | hlink | tlink | (info) |
---|
顶点结点 | data | firstin | firstout |
---|
弧结点中有 5 个域:tailvex 和 headvex 两个域分别指示弧尾和弧头这两个顶点的编号;头链域 hlink 指向弧头相同的下一个弧结点;尾链域 tlink 指向弧尾相同的下一个弧结点;info 域存放该弧的相关信息。这样,弧头相同的弧在同一个链表上,弧尾相同的弧也在同一个链表上。
顶点结点中有 3 个域:data 域存放该顶点的数据信息,如顶点名称;firstin 域指向以该顶点为弧头的第一个弧结点;firstout 域指向以该顶点为弧尾的第一个弧结点。
图为有向图的十字链表表示法
注意,顶点结点之间是顺序存储的,弧结点省略了info域。
在十字链表中,既容易找到 V i V_i Vi为尾的弧,也容易找到 V i V_i Vi为头的弧,因而容易求得顶点的出度和入度。图的十字链表表示是不唯一的,但一个十字链表表示唯一确定一个图。
6.2.4邻接多重表
邻接多重表是无向图
的一种链式存储结构。在邻接表中,容易求得顶点和边的各种信息, 但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。与十字链表类似,在邻接多重表中,每条边用一个结点表示,其结构如下所示。
ivex | ilink | jvex | jlink | (info) |
---|
其中,ivex 和 jvex 这两个域指示该边依附的两个顶点的编号;ilink 域指向下一条依附于顶点 ivex 的边;jlink 域指向下一条依附于顶点 jvex 的边,info 域存放该边的相关信息。
每个顶点也用一个结点表示,它由如下所示的两个域组成。
data | firstedge |
---|
在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,因为每条边依附于两个顶点,所以每个边结点同时链接在两个链表中。对无向图而言,其邻接多重表和邻接表的差别仅在于,同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
图为无向图的邻接多重表表示法。 邻接多重表的各种基本操作的实现和邻接表类似。
图的四种存储方式的总结表:
6.2.5图的基本操作
图的基本操作是独立于图的存储结构的。而对于不同的存储方式,操作算法的具体实现会有着不同的性能。在设计具体算法的实现时,应考虑采用何种存储方式的算法效率会更高。
图的基本操作主要包括(仅抽象地考虑,所以忽略各变量的类型):
- Adjacent(G,x,y):判断图G是否存在边<x,y>或(x,y).
- Neighbors(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中删除该边。
- FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在×,则返回-1.
- NextNeighbor(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.
此外,还有图的遍历算法:按照某种方式访问图中的每个顶点且仅访问一次。图的遍历算法包括深度优先遍历和广度优先遍历。
6.3图的遍历
图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次,且仅访问一次。注意到树是一种特殊的图,所以树的遍历实际上也可视为一种特殊的图的遍历。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。
图的遍历比树的遍历要复杂得多,因为图的任意一个顶点都可能和其余的顶点相邻接,所以在访问某个顶点后,可能沿着某条路径搜索又回到该顶点。为避免同一顶点被访问多次,在遍历图的过程中,必须记下每个已访问过的顶点,为此可以设一个辅助数组 visited[]来标记顶点是否被访问过。图的遍历算法主要有两种:广度优先搜索和深度优先搜索。
6.3.1 广度优先搜索
广度优先搜索(Breadth-First-Search, BFS)类似于二叉树的层序遍历算法。基本思想是:首先访问起始顶点 v v v,接着由 v v v出发,依次访问 v v v的各个未访问过的邻接顶点 w 1 , w 2 , ⋯ , w i w_1,w_2,\cdots,w_i w1,w2,⋯,wi,然后依次访问 w 1 , w 2 , ⋯ , w i w_1,w_2,\cdots,w_i w1,w2,⋯,wi的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直至图中所有顶点都被访问过为止。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止。Dijkstra 单源最短路径算法和 Prim 最小生成树算法也应用了类似的思想。
换句话说,广度优先搜索遍历图的过程是以 v v v为起始点,由近至远依次访问和 v v v有路径相通且路径长度为 1,2,…的顶点。广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。
广度优先搜索算法的伪代码如下:
bool visited[MAX_VERTEX_NUM];
void BFSTraverse(Graph G){
for(int i=0;i<G.vexnum;++i){
visited[i]=FALSE;
}
//辅助队列
InitQueue(Q);
for(int i=0;i<G.vexnum;++i){
if(!visited[i]){
BFS(G,i);
}
}
}
用邻接表实现广度优先搜索的算法如下:
void BFS(ALGraph G,int i){
visit(i);
visited[i]=TRUE;
EnQueue(Q,i);
while(!IsEmpty(Q)){
DeQueue(Q,v);
for(p=G.vertices[v].firstarc;p;p=p->nextarc){
w=p->adjvex;
if(visited[w]==FALSE){
visit(w);
visited[w]=TRUE;
EnQueue(Q,w);
}
}
}
}
用邻接矩阵实现广度优先搜索的算法如下:
void BFS(MGraph G,int i){
visit(i);
visited(i)=TRUE;
EnQueue(Q,i);
while(!IsEmpty(Q)){
DeQueue(Q,v);
for(w=0;w<G.vexnum;w++){
if(vivited[w]==FALSE&&G.edge[v][w]==1){
visit(w);
visited[w]=TRUE;
EnQueue(Q,w);
}
}
}
}
辅助数组 visited[]标志顶点是否被访问过,其初始状态为 FALSE。在图的遍历过程中,一旦某个顶点 v i v_i vi被访问,就立即置 visited[i]为TRUE,防止它被多次访问。
广度优先遍历的过程
下面通过实例演示广度优先搜索的过程,给定无向图 G G G如图所示。
假设从顶点 a a a开始访问, a a a先入队。此时队列非空,取出队头元素 a a a,因为 b , c b,c b,c与 a a a邻接且未被访问过,于是依次访问 b , c b,c b,c,并将 b , c b,c b,c依次入队。队列非空,取出队头元素 b b b,依次访问与 b b b邻接且未被访问的顶点 d , e d,e d,e,并将 d , e d,e d,e入队(注意: a a a与 b b b也邻接,但 a a a已置访问标记,所以不再重复访问)。此时队列非空,取出队头元素 c c c,访问与 c c c邻接且未被访问的顶点 f , g f,g f,g,并将 f , g f,g f,g 入队。此时,取出队头元素 d d d,但与 d d d邻接且未被访问的顶点为空,所以不进行任何操作。继续取出队头元素 e e e,将 h h h 入队列…最终取出队头元素 h h h后,队列为空,从而循环自动跳出。遍历结果为 abcdefgh。
从上例不难看出,图的广度优先搜索的过程与二叉树的层序遍历是完全一致的,这也说明了图的广度优先搜索遍历算法是二叉树的层次遍历算法的扩展。
1.BFS 算法的性能分析
无论是邻接表还是邻接矩阵的存储方式,BFS 算法都需要借助一个辅助队列Q, n n n个顶点均需入队一次,在最坏的情况下,空间复杂度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣)。
基于邻接表存储的 BFS 的效率
遍历图的过程实质上是对每个顶点查找其邻接点的过程,耗费的时间取决于所采用的存储结构。采用邻接表存储时,每个顶点均需搜索(或入队)一次,时间复杂度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣),在搜索每个顶点的邻接点时,每条边至少访问一次,时间复杂度为 O ( ∣ E ∣ ) O(|E|) O(∣E∣),总的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。采用邻接矩阵存储时,查找每个顶点的邻接点所需的时间为 O ( ∣ V ∣ ) O(|V|) O(∣V∣),总时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
2.BFS算法求解单源最短路径问题
若图 G = ( V , E ) G= ( V, E) G=(V,E)为非带权图 , 定义从顶点 u u u 到顶点 ν \nu ν的最短路径 d ( u , v ) d( u, v) d(u,v)为从 u u u 到 ν \nu ν的任何路径中最少的边数;若从 u u u到 ν \nu ν没有通路,则 d ( u , v ) = ∞ d(u,v)=\infty 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;
d[u]=0;
EnQueue(Q,u);
while(!isEmpty(Q)){
Dequeue(Q,u);
for(w=FirstNeighbor(G,u);w>0;w=NextNeighbor(G,u,w)){
if(!visited[w]){
visited[w]=TRUE;
d[w]=d[u]+1;
EnQueue(Q,w);
}
}
}
}
3.广度优先生成树
在广度遍历的过程中,我们可以得到一棵遍历树,称为广度优先生成树,如图所示。需要注意的是,同一个图的邻接矩阵存储表示是唯一的,所以其广度优先生成树也是唯一的, 但因为邻接表存储表示不唯一,所以其广度优先生成树也是不唯一的。
6.3.2深度优先搜索
与广度优先搜索不同,深度优先搜索(Depth-First-Search,DFS)类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的策略是尽可能“深”地搜索一个图。
它的基本思想如下:首先访问图中某一起始顶点 ν \nu ν,然后由 ν \nu ν出发,访问与 ν \nu ν邻接且未被访问的任意一个顶点 w 1 w_1 w1,再访问与 w 1 w_1 w1邻接且未被访问的任意一个顶点 w 2 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ w_2\cdotp\cdotp\cdotp\cdotp\cdotp\cdotp w2⋅⋅⋅⋅⋅⋅重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
一般情况下,其递归形式的算法十分简洁,算法过程如下:
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
for(int i=0;i<G.vexnumk;i++){
visited[i]=FALSE;
}
for(int i=0;i<G.vexnum;i++){
if(!visite[i]){
DFS(G,i);
}
}
}
用邻接表实现深度优先搜索的算法如下:
void BFS(ALGraph G,int i){
visit(i);
visited[i]=TRUE;
EnQueue(Q,i);
for(p=G.vertices[i].firstarc;p;p=p->nextarc){
j=p->adjvex;
if(visited[j]==FALSE){
DFS(G,j);
}
}
用邻接矩阵实现深度优先搜索的算法如下:
void BFS(MGraph G,int i){
visit(i);
visited(i)=TRUE;
EnQueue(Q,i);
for(j=0;j<G.vexnum;j++){
if(vivited[j]==FALSE&&G.edge[i][j]==1){
DFS(G,j);
}
}
}
深度优先遍历的过程
以图 6.3.1中 的无向图为例,深度优先搜索的过程:首先访问 a a a,并置 a a a访问标记;然后访问与 a a a 邻接且未被访问的顶点 b b b,置 b b b访问标记:然后访问与 b b b邻接且未被访问的顶点 d d d,置 d d d访问标记。此时 d d d已没有未被访问过的邻接点,所以返回上一个访问的顶点 b b b,访问与其邻接且未被访问的顶点 e e e,置 e e e访问标记,以此类推,直至图中所有顶点都被访问一次。遍历结果为 abdehcfg。
图的邻接矩阵表示是唯一的,但对邻接表来说,若边的输入次序不同,则生成的邻接表也不同。因此,对同样一个图,基于邻接矩阵的遍历得到的 DFS 序列和 BFS 序列是唯一的, 基于邻接表的遍历得到的 DFS 序列和 BFS 序列是不唯一的。
1. DFS 算法的性能分析
DFS 算法是一个递归算法,需要借助一个递归工作栈,所以其空间复杂度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣)。
遍历图的过程实质上是通过边查找邻接点的过程,因此两种遍历方式的时间复杂度都相同,不同之处仅在于对顶点访问顺序的不同。采用邻接矩阵存储时,总时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。采用邻接表存储时,总的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。
2. 深度优先的生成树和生成森林
与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。当然,这是有条件的, 即对连通图调用 DFS 才能产生深度优先生成树,否则产生的将是深度优先生成森林,如图所示。与 BFS 类似,基于邻接表存储的深度优先生成树是不唯一的。
6.3.3 图的遍历与图的连通性
图的遍历算法可以用来判断图的连通性。对于无向图来说,若无向图是连通的,则从任意一个结点出发,仅需一次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。对于有向图来说,若从初始顶点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点。
因此,在 BFSTraverse()或 DFSTraverse()中添加了第二个 for 循环,再选取初始点,继续进行遍历,以防止一次无法遍历图的所有顶点。对于无向图,上述两个函数调用 BFS(G,i) 或 DES(G,i)的次数等于该图的连通分量数;而对于有向图则不是这样,因为一个连通的有向图分为强连通的和非强连通的,它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次调用 BFS(G,i)或 DFS(G,i)无法访问到该连通分量的所有顶点,如图所示。
6.4图的应用
本节是历年考查的重点。图的应用主要包括:最小生成(代价)树、最短路径、拓扑排序和关键路径。一般而言,这部分内容直接以算法设计题形式考查的可能性极小,而更多的是结合图的实例来考查算法的具体操作过程,读者必须学会手工模拟给定图的各个算法的执行过程。此外,还需掌握对给定模型建立相应的图去解决问题的方法。
6.4.1 最小生成树
一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边。对于生成树来说,若砍去它的一条边,则会使生成树变成非连通图;若给它增加一条边,则会形成图中的一条回路。
对于一个带权连通无向图 G,生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。权值之和最小的那棵生成树称为 G G G 的最小生成树(Minimum-Spanning-Tree,MST)。
最小生成树的性质
不难看出,最小生成树具有如下性质:
-
若图 G G G中存在权值相同的边,则 G G G的最小生成树可能不唯一,即最小生成树的树形不唯一。当图 G G G中的各边权值互不相等时, G G G的最小生成树是唯一的;若无向连通图 G G G的边数比顶点数少 1,即 G G G本身是一棵树时,则 G G G的最小生成树就是它本身。
-
虽然最小生成树不唯一,但其对应的边的权值之和总是唯一的,而且是最小的。
-
最小生成树的边数为顶点数减 1。
最小生成树中某顶点到其他顶点是否具有最短路径的分析
最小生成树中所有边的权值之和最小,但不能保证任意两个顶点之间的路径是最短路径。
如下图所示,最小生成树中 A到C的路径长度为5,但原图中C到D的最短路径长度为4。
构造最小生成树有多种算法,但大多数算法都利用了最小生成树的下列性质:假设 G = ( V , E ) G=(V,E) G=(V,E) 是一个带权连通无向图, U U U 是顶点集 V V V 的一个非空子集。若 ( u , v ) (u,v) (u,v)是一条具有最小权值的边,其中 u ∈ U , v ∈ V − U u\in U,v\in V-U u∈U,v∈V−U,则必存在一棵包含边 ( u , v ) (u,v) (u,v)的最小生成树。
基于该性质的最小生成树算法主要有 Prim 算法和 Kruskal 算法,它们都基于贪心算法的策略。对这两种算法应主要掌握算法的本质含义和基本思想,并能够手工模拟算法的实现步骤。
下面介绍一个通用的最小生成树算法:
GENERIC_MST(G){
T=NULL;
while T 未形成一棵生成树;
do 找到一条最小代价边(u,v)并且加入T后不会产生回路;
T=T∩(u,v)
}
通用算法每次加入一条边以逐渐形成一棵生成树,下面介绍两种实现上述通用算法的途径。
1. Prim 算法
Prim (普里姆)算法的执行非常类似于寻找图的最短路径的 Dijkstra 算法(见下一节)。
Prim 算法构造最小生成树的实例
Prim 算法构造最小生成树的过程如图所示。初始时从图中任取一顶点(如顶点 1)加入树 T T T,此时树中只含有一个顶点,之后选择一个与当前 T T T中顶点集合距离最近的顶点,并将该顶点和相应的边加入 T T T,每次操作后 T T T中的顶点数和边数都增 1。以此类推,直至图中所有的顶点都并入 T T T,得到的 T T T就是最小生成树。此时 T T T中必然有 n − 1 n- 1 n−1条边。
Prim 算法的步骤如下:
假设 G = { V , E } G=\{V,E\} G={V,E}是连通图,其最小生成树 T = ( U , E T ) , E T T=(U,E_T),E_T T=(U,ET),ET是最小生成树中边的集合。
初始化:向空树 T = ( U , E T ) T=(U,E_T) T=(U,ET)中添加图 G = ( V , E ) G=(V,E) G=(V,E)的任意一个顶点 u 0 u_0 u0,使 U = { u 0 } , E T = ∅ U=\{u_0\},E_T=\varnothing U={u0},ET=∅。
循环(重复下列操作直至 U = V ) : U=V): U=V): 从图 G G G 中选择满足 { ( u , v ) ∣ u ∈ U , v ∈ V − U } \{(u,v)|u\in U,v\in V-U\} {(u,v)∣u∈U,v∈V−U}且具有最小权值的边 ( u , v ) (u,v) (u,v),加入树 T T T,置 U = U ∪ { v } , E T = E T ∪ { ( u , v ) } U=U\cup\{v\},E_T=E_T\cup\{(u,v)\} U=U∪{v},ET=ET∪{(u,v)}。
Prim 算法的简单实现如下:
void (G,T){
T=Ø;//初始化空树
U={w};
while((V-U)!=Ø){
设(U,v)是使u∈U与v∈(V-U),且权值最小的边;
T=T∪{(u,v)};
U=U∪{v};
}
}
Prim 算法的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),不依赖于 ∣ E ∣ |E| ∣E∣,因此它适用于求解边稠密的图的最小生成树。虽然采用其他方法能改进 Prim 算法的时间复杂度,但增加了实现的复杂性。
2. Kruskal 算法
与 Prim 算法从顶点开始扩展最小生成树不同,Kruskal 算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。
Kruskal算法构造最小生成树的实例
Kruskal 算法构造最小生成树的过程如图 6.16 所示。初始时为只有 n n n个顶点而无边的非连通图 T = { V , { } } T=\{V,\{\}\} T={V,{}},每个顶点自成一个连通分量。然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在 T T T中不同的连通分量上 (使用并查集判断这两个顶点是否属于同一棵集合树),则将此边加入 T T T,否则舍弃此边而选择下一条权值最小的边。以此类推,直至 T T T中所有顶点都在一个连通分量上。
Kruskal 算法的步骤如下:
假设
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)是连通图,其最小生成树
T
=
(
U
,
E
T
)
T=(U,E_T)
T=(U,ET)。
初始化:
U
=
V
,
E
T
=
∅
U=V,E_T=\varnothing
U=V,ET=∅。即每个顶点构成一棵独立的树,
T
T
T此时是一个仅含
∣
V
∣
|V|
∣V∣个顶点的森林。循环(重复直至
T
T
T是一棵树): 按
G
G
G的边的权值递增顺序依次从
E
−
E
T
E-E_T
E−ET中选择一条边,若这条边加入
T
T
T后不构成回路,则将其加入
E
T
E_T
ET,否则舍弃,直到
E
T
E_T
ET中含有
n
−
1
n-1
n−1条边。
Kruskal 算法的简单实现如下:
void (G,T){
T=V;//初始化
numS=n;
while(numS>1){
从E中取出权值最小的边(v,u);
if(v和u属于T中不同的连通分量){
T=T∪{(u,v)};
numS--;
}
}
}
根据图的相关性质,若一条边连接了两棵不同树中的顶点,则对这两棵树来说,它必定是连通的,将这条边加入森林中,完成两棵树的合并,直到整个森林合并成一棵树。
在 Kruskal 算法中,最坏情况需要对 ∣ E ∣ |E| ∣E∣条边各扫描一次。通常采用堆(见第 7 章)来存放边的集合,每次选择最小权值的边需要 O ( log 2 ∣ E ∣ ) O(\log_2|E|) O(log2∣E∣)的时间;每次使用并查集来快速判断两个顶点是否属于一个集合所需的时间为 O ( α ( ∣ V ∣ ) ) , α ( ∣ V ∣ ) O(\alpha(|V|)),\alpha(|V|) O(α(∣V∣)),α(∣V∣)的增长极其缓慢,可视为常数。算法的总时间复杂度为 O ( ∣ E ∣ log 2 ∣ E ∣ ) O(|E|\log_2|E|) O(∣E∣log2∣E∣),不依赖于 ∣ V ∣ |V| ∣V∣,因此 Kruskal 算法适合于边稀疏而顶点较多的图。
6.4.2 最短路径
最短路径的分析与举例以及相关的算法
6.3 节所述的广度优先搜索查找最短路径只是对无权图而言的。当图是带权图时,把从一个顶点 v 0 v_0 v0到图中其余任意一个顶点 v i v_i vi的一条路径所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径(可能不止一条)称为最短路径。
求解最短路径的算法通常都依赖于一种性质,即两点之间的最短路径也包含了路径上其他顶点间的最短路径。带权有向图 G G G的最短路径问题一般可分为两类:一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可通过经典的 Dijkstra(迪杰斯特拉)算法求解;二是求每对顶点间的最短路径,可通过 Floyd(弗洛伊德)算法来求解。
1.Dijkstra 算法求单源最短路径问题
Dijkstra 算法设置一个集合 S 记录已求得的最短路径的顶点,初始时把源点 v 0 v_{0} v0放入 S,集合S每并入一个新顶点 v i v_i vi,都要修改源点 v 0 v_0 v0到集合 V − S V-S V−S中顶点当前的最短路径长度值(这里可能不太好理解?没关系,继续往下看,相信会逐步理解)。
在构造的过程中还设置了三个辅助数组:
- final[]:标记各顶点是否已找到最短路径,即是否归入集合 S。
- dist[]:记录从源点 ν 0 \nu_{0} ν0到其他各顶点当前的最短路径长度,它的初始值为:若从 ν 0 \nu_{0} ν0到 ν i \nu_i νi有弧,则 dist[i]为弧上的权值;否则置 dist[i]为∞。
- path[]: path[i]表示从源点到顶点 i i i之间的最短路径的前驱结点。在算法结束时,可根据其值追溯得到源点 ν 0 \nu_0 ν0到顶点 ν i \nu_i νi的最短路径。
假设从顶点0出发,即 v 0 = 0 v_0=0 v0=0,集合S最初只包含顶点0,邻接矩阵arcs表示带权有向图, arcs[i][j] \operatorname{arcs[i][j]} arcs[i][j]表示有向边< i , j i,j i,j>的权值,若不存在有向边< i , j > i,j> i,j>,则 arcs[i][j] \operatorname{arcs[i][j]} arcs[i][j]为∞。
Dijkstra算法的步骤如下(不考虑对path[]的操作):
-
初始化:集合S初始为{0},dist[]的初始值dist[i]=arcs[0][i],i=1,2,⋯,n-1。
-
从顶点集合 V − S V-S V−S中选出 v j v_j vj,满足 d i s t [ j ] = M i n { d i s t [ i ] ∣ v i ∈ V − S } , v j dist[j]=Min\{dist[i]|v_i\in V-S\},v_{j} dist[j]=Min{dist[i]∣vi∈V−S},vj就是当前求得的一条从 v 0 v_{0} v0出发的最短路径的终点,令 S = S ∪ { j } S=S\cup\{j\} S=S∪{j}。
-
修改从 v 0 v_{0} v0出发到集合 V − S V-S V−S上任意一个顶点 v k v_{k} vk可达的最短路径长度:若dist[j]+arcs[j][k]<dist[k],则更新dist[k]=dist[j]+arcs[j][k]。
这里就是比较v0直接到达的结点的权值是否比以vj为前驱结点的权值要大,对于目前记录的所有路径中的权值是否小于v0到当前结点vj与vj相连节点的权值的和
-
重复2一3操作共 n − 1 n-1 n−1次,直到所有的顶点都包含在集合S中。
步骤 3)也就是开头留下的疑问,每当一个顶点加入集合 S 后,可能需要修改源点 ν 0 \nu_{0} ν0到集合 V − S V-S V−S中可达顶点当前的最短路径长度,下面举一简单例子证明。如下图所示,源点为 v 0 v_0 v0,初始时 S = { v 0 } S=\{\boldsymbol{v}_0\} S={v0}, dist[1]=3, dist[2]=7,当将 v l v_\mathrm{l} vl并入集合 S后,dist[2]需要更新为4。
思考:Dijkstra算法与Prim算法有何异同之处?
Dijkstra算法求解最短路径的实例
例如,对例图中的图应用Dijkstra算法求从顶点1出发至其余顶点的最短路径的过程,如表所示。算法执行过程的说明如下。
初始化:集合 S 初始为 { v 1 } , v 1 \{v_1\},v_1 {v1},v1可达 v 2 v_2 v2和 v 5 , v 1 v_5,v_1 v5,v1不可达 v 3 v_3 v3和 v 4 v_4 v4, 因此 dist[]数组各元素的初始值依次设置为 dist[2]=10, dist[3]=∞, dist[4]=∞, dist[5]=5。
第 1轮:选出最小值 dist[5],将顶点
v
5
v_5
v5并入集合 S, 即此时已找到
ν
1
\nu_1
ν1到
v
5
v_5
v5的最短路径。当
v
5
v_{5}
v5加入 S后,从
v
1
v_1
v1到集合
V
−
S
V-S
V−S 中可达顶点的最短路径长度可能会产生变化。因此需要更新 dist[]数组。
v
5
v_5
v5可达
v
2
v_2
v2,因
v
1
→
v
5
→
v
2
v_1\to v_5\to v_2
v1→v5→v2的距离 8 比 dist[2]=10 小,更新 dist[2]=8;
v
5
v_5
v5可达
v
3
v_3
v3,
v
1
→
v
5
→
v
3
v_1\to v_5\to v_3
v1→v5→v3的距离 14,更新 dist[3]=14;
v
5
v_5
v5可达
v
4
v_4
v4,
v
1
→
v
5
→
v
4
v_1\to v_5\to v_4
v1→v5→v4的 距离 7, 更新dist[ 4] = 7。
第 2 轮:选出最小值 dist[4],将顶点 v 4 v_{4} v4并入集合 S。继续更新 dist[]数组。 v 4 v_{4} v4不可达 v 2 v_2 v2, dist[2]不变; v 4 v_4 v4可达 v 3 , v 1 → v 5 → v 4 → v 3 v_3,v_1\to v_5\to v_4\to v_3 v3,v1→v5→v4→v3的距离 13比 dist[3]小,故更新 dist[3]=13。
第 3轮:选出最小值 dist[2],将顶点 v 2 v_{2} v2并入集合 S。继续更新 dist[]数组。 v 2 v_2 v2可达 v 3 v_3 v3, v 1 → v 5 → v 2 → v 3 v_1\to v_5\to v_2\to v_3 v1→v5→v2→v3的距离 9比 dist[3]小,更新dist[3]=9。
第 4轮:选出唯一最小值 dist[3],将顶点 v 3 v_{3} v3并入集合 S, 此时全部顶点都已包含在 S中。
显然,Dijkstra 算法也是基于贪心策略的。
使用邻接矩阵表示时,时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。使用带权的邻接表表示时,虽然修改 dist[] 的时间可以减少,但由于在 dist[]中选择最小分量的时间不变,所以时间复杂度仍为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
人们可能只希望找到从源点到某个特定顶点的最短路径,但这个问题和求解源点到其他所有顶点的最短路径一样复杂,时间复杂度也为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
注意,边上带有负权值时,Dijkstra 算法并不适用。若允许边上带有负权值,则在与集合 S (已求得最短路径的顶点集,归入 S 内的顶点的最短路径不再变更)内某顶点(记为
a
a
a)以负边相连的顶点(记为
b
b
b)确定其最短路径时,其最短路径长度加上这条负边的权值结果可能小于
a
a
a 原先确定的最短路径长度,而此时
a
a
a在 Dijkstra算法下是无法更新的。例如,对于例图所示的带权有向图,利用 Dijkstra 算法不一定能得到正确的结果。
2.Floyd 算法求各项点之间最短路径问题
求所有顶点之间的最短路径问题描述如下:已知一个各边权们均大于 0 的带权有向图,对任意两个顶点 v i ≠ v j v_i\neq v_j vi=vj,要求求出 v i v_i vi与 v j v_j vj之间的最短路径和最短路径长度。
Floyd 第法的基本思想是:递推产生一个 n n n阶方阵序列 A ( − 1 ) , A ( 0 ) , ⋯ , A ( k ) , ⋯ , A ( n − 1 ) A^{(-1)},A^{(0)},\cdots,A^{(k)},\cdots,A^{(n-1)} A(−1),A(0),⋯,A(k),⋯,A(n−1),其中 A ( k ) [ i ] [ j ] A^{(k)}[i][j] A(k)[i][j]表示从顶点 v i v_i vi到顶点 v j v_j vj的路径长度, k k k 求示绕行第 k k k 个顶点的运算步骤。初始时,对于任意两个顶点 v i v_i vi和 v j v_j vj,若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在有向边,则以 ∞ \infty ∞作为它们之间的最短路径长度。以后逐步尝试在原路径中加入顶点 k ( k = 0 , 1 , ⋯ , n − 1 ) k(k=0,1,\cdots,n-1) k(k=0,1,⋯,n−1)作为中间顶点。若增加中间顶点后,得到的路径比原来的路径长度减少了,则以此新路径代替原路径。第法描述如下:
定义一个
n
n
n阶方阵序列
A
(
−
1
)
,
A
(
0
)
,
⋯
,
A
(
n
−
1
)
A^{(-1)},A^{(0)},\cdots,A^{(n-1)}
A(−1),A(0),⋯,A(n−1),其中,
A
(
−
1
)
[
i
]
[
j
]
=
a
r
c
s
[
i
]
[
j
]
A
(
k
)
[
i
]
[
j
]
=
M
i
n
{
A
(
k
−
1
)
[
i
]
[
j
]
,
A
(
k
−
1
)
[
i
]
[
k
]
+
A
(
k
−
1
)
[
k
]
[
j
]
}
,
k
=
0
,
1
,
⋯
,
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, \cdots , 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
]
A^{(0)}[i][j]
A(0)[i][j]是从顶点
v
i
v_i
vi,到
v
j
v_j
vj、中间顶点是
v
0
v_0
v0的最短路径的长度,
A
(
k
)
[
i
]
[
j
]
A^{(k)}[i][j]
A(k)[i][j]足从顶点
v
i
v_i
vi,到
v
j
v_j
vj、中间顶点的序号不大于
k
k
k的最短路径的长度。Floyd 第法是一个迭代的过程,每迷代一次,在从
v
i
v_i
vi 到
v
j
v_j
vj,的最短路径上就多考虑了一个顶点:经过
n
n
n 次选代后,所得到的
A
(
n
−
1
)
[
i
]
[
j
]
A^{(n-1)}[i][j]
A(n−1)[i][j]就是
v
i
v_i
vi,到
v
j
v_j
vj,的最短路径长度,即方阵
A
(
n
−
1
)
A^{(n-1)}
A(n−1)中就保存了任意一对顶点之间的最短路径长度。
例图所示为带权有向图 G G G及其邻接矩阵。应用 Floyd 算法求所有顶点之间的最短路径长度的过程如表所示。算法执行过程的说明如下。
初始化:方阵 A ( − 1 ) [ i ] [ j ] = a r c s [ i ] [ j ] A^{(-1)}[i][j]=arcs[i][j] A(−1)[i][j]=arcs[i][j]。
第 1轮:将 v 0 v_0 v0作为中间顶点,对于所有顶点对 { i , j } \{i,j\} {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[i][j]>A−1[i][0]+A−1[0][j],则将 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]更新为 A − 1 [ i ] [ 0 ] + A − 1 [ 0 ] [ j ] A^{-1}[i][0]+A^{-1}[0][j] A−1[i][0]+A−1[0][j]。有 A − 1 [ 2 ] [ 1 ] > A − 1 [ 2 ] [ 0 ] + A − 1 [ 0 ] [ 1 ] = 11 A^{-1}[2][1]>A^{-1}[2][0]+A^{-1}[0][1]=11 A−1[2][1]>A−1[2][0]+A−1[0][1]=11,更新 A − 1 [ 2 ] [ 1 ] = 11 A^{-1}[2][1]=11 A−1[2][1]=11,更新后的方阵标记为 A 0 . A^0. A0.
第 2轮:将 v 1 v_{1} v1作为中间顶点,继续检测全部顶点对 { i , j } \{i,j\} {i,j}。有 A 0 [ 0 ] [ 2 ] > A 0 [ 0 ] [ 1 ] + A 0 [ 1 ] [ 2 ] = 10 A^0[0][2]>A^0[0][1]+A^0[1][2]=10 A0[0][2]>A0[0][1]+A0[1][2]=10,更新 A 0 [ 0 ] [ 2 ] = 10 A^0[0][2]=10 A0[0][2]=10,更新后的方阵标记为 A l A^{\mathrm{l}} Al。
第 3轮:将 v 2 v_2 v2作为中间顶点,继续检测全部顶点对 { i , j } \{i,j\} {i,j}。有 A 1 [ 1 ] [ 0 ] > A 1 [ 1 ] [ 2 ] + A 1 [ 2 ] [ 0 ] = 9 A^1[1][0]>A^1[1][2]+A^1[2][0]=9 A1[1][0]>A1[1][2]+A1[2][0]=9,更新 A 1 [ 1 ] [ 0 ] = 9 A^{1}[1][0]=9 A1[1][0]=9,更新后的方阵标记为 A 2 A^2 A2。此时 A 2 A^2 A2中保存的就是任意顶点对的最短路径长度。
Floyd 算法的时间复杂度为 O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)。不过由于其代码很紧凑,且并不包含其他复杂的数据结构,因此隐含的常数系数是很小的,即使对于中等规模的输入来说,它仍然是相当有效的。
Floyd 算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路。Floyd 算法同样适用于带权无向图,因为带权无向图可视为权值相同往返二重边的有向图。
也可以用单源最短路径算法来解决每对顶点之间的最短路径问题。轮流将每个顶点作为源点,并且在所有边权值均非负时,运行一次 Dijkstra 算法,其时间复杂度为 O ( ∣ V ∣ 2 ) ⋅ ∣ V ∣ = O ( ∣ V ∣ 3 ) O(|V|^2)\cdot|V|=O(|V|^3) O(∣V∣2)⋅∣V∣=O(∣V∣3)。
BFS 算法、Dijkstra 算法和 Floyd 算法求最短路径的总结如表所示。
6.4.3 有向无环图描述表达式
有向无环图:若一个有向图中不存在环,则称为有向无环图,简称 DAG图。
构建表达式的有向无环图
有向无环图是描述含有公共子式的表达式的有效工具。例如表达式
(
(
a
+
b
)
∗
(
b
∗
(
c
+
d
)
)
+
(
c
+
d
)
∗
e
)
∗
(
(
c
+
d
)
∗
e
)
((a+b)^*(b^*(c+d))+(c+d)^*e)^*((c+d)^*e)
((a+b)∗(b∗(c+d))+(c+d)∗e)∗((c+d)∗e)
可以用上一章描述的二叉树来表示,如图所示。仔细观察该表式,可发现有一些相同的子表达式 ( c + d ) (c+d) (c+d)和 ( c + d ) ∗ e (c+d)^*e (c+d)∗e,而在二叉树中,它们也重复出现。若利用有向无环图,则可实现对相同子式的共享,从而节省存储空间,图所示为该表达式的有向无环图表示。
在表达式的有向无环图表示中,不可能出现重复的操作数顶点。
6.4.4拓扑排序
AOV 网:若用有向无环图表示一个工程,其顶点表示活动,用有向边 < V i , V j > <V_i,V_j> <Vi,Vj>表示活动 V i V_i Vi必须先于活动 V j V_{j} Vj进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,简称 AOV 网。在 AOV 网中,活动 V i V_i Vi是活动 V j V_j Vj的直接前驱, V j V_j Vj是 V i V_i Vi的直接后继,这种前驱和后继关系具有传递性,且任何活动 V i V_i Vi不能以它自己作为自己的前驱或后继。
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
- 每个顶点出现且只出现一次。
- 若顶点 A A A在序列中排在顶点 B B B的前面,则在图中不存在从 B B B到 A A A的路径。
或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点 A A A到顶点 B B B的路径,则在排序中 B B B出现在 A A A的后面。每个 AOV 网都有一个或多个拓扑排序序列。
拓扑排序和回路的关系
对一个 AOV 网进行拓扑排序的算法有很多,下面介绍比较常用的一种方法的步骤:
- 从 AOV网中选择一个没有前驱(入度为 0)的顶点并输出。
- 从网中删除该顶点和所有以它为起点的有向边。
- 重复1.和2.直到当前的 AOV 网为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
拓扑排序的实例
图 6.22 所示为拓扑排序过程的示例。每轮选择一个入度为 0 的顶点并输出,然后删除该顶点和所有以它为起点的有向边,最后得到拓扑排序的结果为{1,2,4,3,5}。
拓扑排序算法的实现如下:
bool TopologicalSort(Graph G){
InitStack(S);
int i;
for(i=0;i<G.vexnum;i++){
//入度为零的结点入栈
if(indegree[i]==0){
Push(S,i);
}
}
//已输出的结点数
int count=0;
while(!IsEmpty(S)){
Pop(S,i);
print[count++]=i;
for(p=G.vertices[i].firstarc;p;p=p->nextarc){
v=p->adjvex;
if(!(--indegree[v])){
//入度为零入栈
Push(S,v);
}
}
if(count<G.vexnum){
return false;
}
else{
return true;
}
}
}
不同存储方式下的拓扑排序的效率
因为输出每个顶点的同时还要删除以它为起点的边,所以采用邻接表存储时拓扑排序的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣),采用邻接矩阵存储时拓扑排序的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
DFS实现拓扑排序的思想
此外,利用上一节的深度优先遍历也可以实现拓扑排序,下面简单描述其思路。对于有向无环图 G G G 中的任意结点 u , ν u,\nu u,ν,它们之间的关系必然是下列三种之一:
- 若 u u u 是 ν \nu ν 的祖先,则在调用 DFS 访问 u u u之前,必然已对 ν \nu ν进行了 DFS 访问,即 ν \nu ν的 DFS结束时间先于 u u u的 DFS 结束时间。从而可考虑在 DFS 函数中设置一个时间标记,在DFS 调用结束时,对各顶点计时。因此,祖先的结束时间必然大于子孙的结束时间。
- 若u是v的子孙,则v为u的祖先,按上述思路,v的结束时间大于u的结束时间。
- 若u和v没有路径关系,则u和v在拓扑序列的关系任意。
于是,按结束时间从大到小排列,就可以得到一个拓扑排序序列。对一个AOV网,若采用下列步骤进行排序,则称之为逆拓扑排序:- 从AOV网中选择一个没有后继(出度为0)的顶点并输出。
- 从网中删除该顶点和所有以它为终点的有向边。
- 重复1.和2.直到当前的AOV网为空。
用拓扑排序算法处理 AOV 网时,应注意以下问题:
- 入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。
分析给定图的拓扑序列的存在性和唯一性
- 若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一;但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。
- 由于 AOV 网中各顶点的地位平等,每个顶点编号是人为的,因此可以按拓扑排序的结果重新编号,生成 AOV 网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立。
6.4.5 关键路径
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称 AOE网。AOE 网和 AOV网都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE 网中的边有权值;而 AOV 网中的边无权值,仅表示顶点之间的前后关系。
AOE 网具有以下两个性质:
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
在 AOE 网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;也仅有一个出度为 0 的顶点,称为结束顶点(汇点),它表示整个工程的结束。
关键路径的性质
在 AOE 网中,有些活动是可以并行进行的。从源点到汇点的有向路径可能有多条,并且这不同。完成不同路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成,则整个工程的完成时间就会延长。因此。只要找到了关键话动,就找到了关键路径,也就可以得出最短完成时间。下面给出在寻找关键活动时所用到的几个参量的定义。
1.事件 ν k \nu_{k} νk的最早发生时间 ν k ( k ) \nu_{k}(k) νk(k)
它是指从源点 ν 1 \nu_{1} ν1到顶点 ν k \nu_{k} νk的最长路径长度。事件 ν k \nu_{k} νk的最早发生时间决定了所有从 ν k \nu_{k} νk开始的活动能够开工的最早时间。可用下面的递推公式来计算:
v e ( 源点 ) = 0 v_{e}(源点)=0 ve(源点)=0
v e ( k ) = M a x { v e ( j ) + W e i g h t ( v j , v k ) } , v k v_{e}(k)=\mathrm{Max}\{v_{e}(j)+\mathrm{Weight}(v_{j},v_{k})\},v_{k} ve(k)=Max{ve(j)+Weight(vj,vk)},vk为 v j v_{j} vj的任意后继, W e i g h t ( v j , v k ) Weight(v_{j},v_{k}) Weight(vj,vk)表示 < v j , v k > <v_{j},v_{k}> <vj,vk>上的权值
计算 v e ( ) v_{e}() ve()值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:
- 初始时,令 v e [ 1... n ] = 0 v_e[1...n]=0 ve[1...n]=0。
- 输出一个入度为0的顶点 ν j \nu_j νj时,计算它所有直接后继顶点 ν k \nu_k νk的最早发生时间,若 v e [ j ] + v_e[j]+ ve[j]+Weight ( v j , v k ) > v e [ k ] (v_j,v_k)>v_e[k] (vj,vk)>ve[k],则 v e [ k ] = v e [ j ] + v_e[k]=v_e[j]+ ve[k]=ve[j]+Weight ( v j , v k ) (v_j,v_k) (vj,vk)。以此类推,直至输出全部顶点。
2.事件 ν k \nu_k νk的最迟发生时间 ν t ( k ) \nu_t(k) νt(k)
它是指在不推迟整个工程完成的前提下,即保证它的后继事件 v j v_j vj在其最迟发生时间 v l ( j ) v_l(j) vl(j)能够发生时,该事件最迟必须发生的时间。可用下面的递推公式来计算:
v l ( 汇点 ) = v e ( 汇点 ) v_{l}(汇点)=v_e(汇点) vl(汇点)=ve(汇点)
v l ( k ) = M i n { v l ( j ) − W e i g h t ( v k , v j ) } , v k v_{l}(k)=Min\{v_{l}(j)-Weight(v_k,v_{j})\},v_{k} vl(k)=Min{vl(j)−Weight(vk,vj)},vk为 v j v_{j} vj的任意前驱
计算 ν k ( k ) \nu_k(k) νk(k)值时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算。增设一个栈以记录拓扑序列,拓扑排序结束后从栈顶至栈底便为逆拓扑有序序列。过程如下:
- 初始时,令 v l [ 1... n ] = v e [ n ] v_l[1...n]=v_e[n] vl[1...n]=ve[n]。
- 栈顶顶点 ν j \nu_j νj出栈,计算其所有直接前驱顶点 ν k \nu_k νk的最迟发生时间,若 ν l [ j ] − \nu_l[j]- νl[j]− Weight ( ν k , ν j ) < v l [ k ] (\nu_k,\nu_j)< v_l[k] (νk,νj)<vl[k],则 v l [ k ] = v l [ j ] − W e i g h t ( v k , v j ) v_l[k]=v_l[j]-Weight(v_k,v_j) vl[k]=vl[j]−Weight(vk,vj)。以此类推,直至输出全部栈中顶点。
3.活动 a i a_i ai的最早开始时间 e ( i ) e(i) e(i)
它是指该活动弧的起点所表示的事件的最早发生时间。若边< ν k , ν j \nu_k,\nu_j νk,νj>表示活动 a i a_i ai,则有 e ( i ) = v e ( k ) e(i)=v_e(k) e(i)=ve(k)。
4.活动 a i a_i ai的最迟开始时间 l ( i ) l(i) l(i)
它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。若边< v k , v j v_k,v_j vk,vj>表示活动 a i a_i ai,则有 l ( i ) = v l ( j ) − W e i g h t ( v k , v j ) l(i)=v_{l}(j)-Weight(v_k,v_j) l(i)=vl(j)−Weight(vk,vj)。
5.一个活动 a i a_i ai的最迟开始时间 l ( i ) l(i) l(i)和其最早开始时间 e ( i ) e(i) e(i)的差额 d ( i ) = l ( i ) − e ( i ) d(i)=l(i)-e(i) d(i)=l(i)−e(i)
它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动 a i a_i ai可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称 l ( i ) − e ( i ) = 0 l(i)-e(i)=0 l(i)−e(i)=0 即 l ( i ) = e ( i ) l(i)=e(i) l(i)=e(i)的活动 a i a_i ai是关键活动。
求关键路径的实例
求关键路径的算法步骤如下:
- 从源点出发,令 v e ( v_e( ve(源点)=0,按拓扑有序求其余顶点的最早发生时间 v e ( ) 。 v_e()。 ve()。
- 从汇点出发,令 v l ( 汇点 ) = v e ( 汇点 ) v_{l}( 汇 点 ) = v_{e}(汇点) vl(汇点)=ve(汇点) , 按进拓扑有序求其余项 点的最迟发生时间 $v_{l}( ) $。
- 根据各顶点的 v e ( ) v_{e}() ve()值求所有孤的最早开始时间 e ( ) e() e() 。
- 根据各顶点的 v l ( ) v_{l}() vl()值求所有孤的最迟开始时间 l ( ) l() l()。
- 求 AOE 网中所有活动的差额 d ( ) d() d(), 找出所有 d ( ) = 0 d()=0 d()=0 。
图所示为求解关键路径的过程,简单说明如下:
- 求 v e ( ) : v_e(): ve():初始 v e ( 1 ) = 0 v_e(1)=0 ve(1)=0,在拓扑排序输出顶点过程中,求得 v e ( 2 ) = 3 , v e ( 3 ) = 2 v_e(2)=3,v_e(3)=2 ve(2)=3,ve(3)=2, v e ( 4 ) = max { ν c ( 2 ) + 2 , ν c ( 3 ) + 4 } = max { 5 , 6 } = 6 v_e(4)=\max \left \{ \nu _{c}( 2) + 2, \nu _{c}( 3) + 4\right \} = \max \left \{ 5, 6\right \} = 6 ve(4)=max{νc(2)+2,νc(3)+4}=max{5,6}=6, ν c ( 5 ) = 6 \nu _{c}( 5) = 6 νc(5)=6, ν c ( 6 ) = max { ν c ( 5 ) + 1 , ν c ( 4 ) + 2 , ν d ( 3 ) + 3 } = max { 7 , 8 , 5 } = 8 \nu _{c}( 6) = \max \{ \nu _{c}( 5) + 1, \nu _{c}( 4) + 2, \nu _{d}( 3) +3\}=\max\{7,8,5\}=8 νc(6)=max{νc(5)+1,νc(4)+2,νd(3)+3}=max{7,8,5}=8。
若这是一道选择题,根据上述求 ν e ( ) \nu_e() νe()的过程就已经能知道关键路径。
- 求 v l ( ) v_l() vl(): 初始 v ( 6 ) = 8 v(6)=8 v(6)=8,在逆拓扑排序出栈过程中,求得 v l ( 5 ) = 7 v_l( 5) = 7 vl(5)=7, v l ( 4 ) = 6 v_l( 4) = 6 vl(4)=6, v l ( 3 ) = min { v l ( 4 ) − 4 , v l ( 6 ) − 3 } = min { 2 , 5 } = 2 v_l( 3) = \min \{ v_l( 4) - 4, v_l( 6) - 3\} = \min \{ 2, 5\} = 2 vl(3)=min{vl(4)−4,vl(6)−3}=min{2,5}=2, v l ( 2 ) = min { v l ( 5 ) − 3 v_l( 2) = \min \{ v_l( 5) - 3 vl(2)=min{vl(5)−3, v l ( 4 ) − 2 } = min { 4 , 4 } v_l( 4) - 2\} = \min \{ 4,4\} vl(4)−2}=min{4,4}, v l ( 1 ) v_l( 1) vl(1)必然为0而无须再求。
- 弧的最早开始时间 e e e()等于该弧的起点的顶点的 ν e \nu_e νe(),结果如下表。
- 弧的最迟开始时间 l ( i ) l(i) l(i)等于该弧的终点的顶点的 v l ( ) v_l() vl()减去该弧持续的时间,结果如下表。
- 根据 l ( i ) − e ( i ) = 0 l(i)-e(i)=0 l(i)−e(i)=0的关键活动,得到的关键路径为 ( v 1 , v 3 , v 4 , v 6 ) (v_1,v_3,v_4,v_6) (v1,v3,v4,v6)。
缩短工期的相关分析
对于关键路径,需要注意以下几点:
- 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可以通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能会变成非关键活动。
- 网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。
各种图算法在采用邻接矩阵或邻接表存储时的时间复杂度如表 6.5 所示。