这是本人根据王道考研数据结构课程整理的笔记,希望对您有帮助。
6.1 图
图的定义
图
G
G
G 由顶点集
V
V
V 和边集
E
E
E 组成,记为
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),其中 $ 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,\dots,v_n\}
V={v1,v2,…,vn},则用
∣
V
∣
\left| V \right|
∣V∣ 表示图
G
G
G 中顶点的个数,也称图
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
∣
\left| E \right|
∣E∣ 表示图
G
G
G 中边的条数。
无向图
若
E
E
E 是无向边(简称边)的有限集合时,则图
G
G
G 为无向图。边是顶点的无序对,记为
(
v
,
w
)
(v,w)
(v,w) 或
(
w
,
v
)
(w,v)
(w,v) ,因为
(
v
,
w
)
=
(
w
,
v
)
(v,w)=(w,v)
(v,w)=(w,v),其中
v
,
w
v,w
v,w 是顶点。可以说顶点
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 相关联。
G
1
=
(
V
1
,
E
1
)
V
1
=
{
A
,
B
,
C
,
D
,
E
}
E
1
=
{
(
A
,
B
)
,
(
B
,
D
)
,
(
B
,
E
)
,
(
C
,
D
)
,
(
C
,
E
)
,
(
D
,
E
)
}
G_1=(V_1,E_1)\\ V_1=\{A,B,C,D,E \} \\ E_1=\{(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)\}
G1=(V1,E1)V1={A,B,C,D,E}E1={(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)}
有向图
若
E
E
E 是有向边(简称弧)的有限集合时,则图
G
G
G 为有向图。弧是顶点的有序对,记为
⟨
v
,
w
⟩
\lang v,w \rang
⟨v,w⟩ ,其中
v
,
w
v,w
v,w 是顶点,
v
v
v 称为弧尾,
w
w
w称为弧头。
⟨
v
,
w
⟩
\langle v,w \rangle
⟨v,w⟩称为从顶点
v
v
v 到顶点
w
w
w 的弧,也称
v
v
v 邻接到
w
w
w,或
w
w
w 邻接自
v
v
v。$ \lang v,w \rang\neq \lang w,v\rang $。
G
2
=
(
V
2
,
E
2
)
V
2
=
{
A
,
B
,
C
,
D
,
E
}
E
2
=
{
⟨
A
,
B
⟩
,
⟨
A
,
C
⟩
,
⟨
A
,
D
⟩
,
⟨
A
,
E
⟩
,
⟨
B
,
A
⟩
,
⟨
B
,
C
⟩
,
⟨
B
,
E
⟩
,
⟨
C
,
D
⟩
}
G_2=(V_2,E_2)\\ V_2=\{A,B,C,D,E \} \\ E_2=\{\lang A,B \rang,\lang A,C \rang,\lang A,D \rang,\lang A,E \rang,\lang B,A \rang,\lang B,C \rang,\lang B,E \rang,\lang C,D \rang\}
G2=(V2,E2)V2={A,B,C,D,E}E2={⟨A,B⟩,⟨A,C⟩,⟨A,D⟩,⟨A,E⟩,⟨B,A⟩,⟨B,C⟩,⟨B,E⟩,⟨C,D⟩}
顶点的度、入度、出度
- 对于无向图:
- 顶点 v v v 的度是指依附于该顶点的边的条数,记为 TD ( v ) \text{TD}(v) TD(v)
- 在具有 n n n 个顶点、 e e e 条边的无向图中, ∑ i = 1 n TD ( v i ) = 2 e \sum_{i=1}^n{\text{TD}(v_i)}=2e ∑i=1nTD(vi)=2e,即无向图的全部顶点的度的和等于边数的2倍
- 对于有向图:
- 入度是以顶点 v v v 为终点的有向边的数目,记为 ID ( v ) \text{ID}(v) ID(v)
- 出度是以顶点 v v v 为起点的有向边的数目,记为 OD ( v ) \text{OD}(v) OD(v)
- 顶点 v v v 的度的等于其入度和出度之和,即 TD ( v ) = ID ( v ) + OD ( v ) \text{TD}(v) = \text{ID}(v)+\text{OD}(v) TD(v)=ID(v)+OD(v)
- 在具有 n n n 个顶点、 e e e 条边的有向图中, ∑ i = 1 n ID ( v i ) = ∑ i = 1 n OD ( v i ) = e \sum_{i=1}^n{\text{ID}(v_i)}=\sum_{i=1}^n{\text{OD}(v_i)}=e ∑i=1nID(vi)=∑i=1nOD(vi)=e
顶点-顶点的关系描述
路径:顶点 v p v_p vp 到顶点 v q v_q vq 之间的一条路径是指顶点序列, v p , v i 1 , v i 2 , … , v i m , v q v_p,v_{i_1},v_{i_2},\dots,v_{i_m},v_{q} vp,vi1,vi2,…,vim,vq
回路:第一个顶点和最后一个顶点相同的路径称为回路或环( n n n 个顶点的图,若 ∣ E ∣ > n − 1 \left| E \right|>n-1 ∣E∣>n−1,则一定有回路)
简单路径:在路径序列中,顶点不重复出现的路径称为简单路径
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路
路径长度:路径上边的数目
点到点的距离:从顶点 u u u 出发到顶点 v v v 的最短路径若存在,则此路径的长度称为从 u u u 到 v v v 的距离。若从 u u u 到 v v v 根本不存在路径,则记该距离为无穷( ∞ \infty ∞)
无向图中,若从顶点 v v v 到顶点 w w w 有路径存在,则称 v v v 和 w w w 是连通的
有向图中,若从顶点 v v v 到顶点 w w w 和从顶点 w w w 到顶点 v v v 之间都有路径,则称这两个顶点是强连通的。
连通图、强连通图
连通图:(无向)图中任意两个顶点都是连通的
强连通图:(有向)图中任意一对顶点都是强连通的
子图、生成子图
生成子图:原图所有的顶点 + 部分边
连通分量
无向图的极大连通子图(子图必须相连,且包含尽可能多的顶点和边)称为连通分量
有向图的极大强连通子图(子图必须强连通,且包含尽可能多的顶点和边)称为强连通分量
生成树
连通图的生成树是包含图中全部顶点的一个极小连通子图(边要尽可能的少,但要保持连通)
若图中顶点数为
n
n
n,则它的生成树含有
n
−
1
n-1
n−1 条边。
生成森林
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
边的权、带权图/网
边的权:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图/网:边上带有权值的图称为带权图,也称网。
带权路径长度:当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
完全图
无向完全图:无向图中任意两个顶点之间都存在边
若无向图的顶点数 ∣ V ∣ = n \left| V\right|=n ∣V∣=n,则 ∣ E ∣ ∈ [ 0 , C n 2 ] = [ 0 , n ( n − 1 ) / 2 ] |E| \in\left[0, C_{n}^{2}\right]=[0, n(n-1) / 2] ∣E∣∈[0,Cn2]=[0,n(n−1)/2]
有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧
若有向图的顶点数 ∣ V ∣ = n \left| V\right|=n ∣V∣=n,则 ∣ E ∣ ∈ [ 0 , 2 C n 2 ] = [ 0 , n ( n − 1 ) ] |E| \in\left[0, 2C_{n}^{2}\right]=[0, n(n-1)] ∣E∣∈[0,2Cn2]=[0,n(n−1)]
稀疏/稠密图
稀疏图:边数很少的图
稠密图:边数很多的图
树
树:不存在回路,且连通的无向图( n n n 个顶点的树,必有 n − 1 n-1 n−1 条边)
有向树:一个顶点的入度为0,其余顶点的入度均为1的有向图,称为有向树。
6.2 图的存储及基本操作
6.2.1 邻接矩阵法
邻接矩阵法
#define MaxVertexNum 100 //顶点数目的最大值
typedef struct
{
char Vex[MaxVertexNum]; //顶点表
int/bool Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表
int vexnum,arcnum; //图的当前顶点数和边数/弧数
}MGraph;
邻接矩阵法存储带权图
有的时候会把自己指向自己的边的权值设为0:
#include <climits>
#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY INT_MAX //宏定义常量“无穷”
typedef struct
{
char Vex[MaxVertexNum]; //顶点
int Edge[MaxVertexNum][MaxVertexNum]; //边的权
int vexnum,arcnum; //图的当前顶点数和边数/弧数
}MGraph;
邻接矩阵法的性能分析
- 空间复杂度: O ( ∣ V ∣ 2 ) O(\left| V\right|^2) O(∣V∣2)——只和顶点数相关,和实际的边数无关
- 适合用于存储稠密图
- 无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)
邻接矩阵法的性质
设图 G G G 的邻接矩阵为 A A A (矩阵元素为0/1),则 A n A^n An 的元素 A n [ i ] [ j ] A^n[i][j] An[i][j] 等于由顶点 i i i 到顶点 j j j 的长度为 n n n 的路径的数目。
6.2.2 邻接表法
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //定义顶点类型
//【边/弧】
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;
邻接表的性质
6.2.3 十字链表法(有向图)
缺点:删除操作不方便
6.2.4 邻接多重表(无向图)
总结:
6.2.5 图的基本操作
Adjacent(G,x,y)
:判断图
G
G
G 是否存在边
⟨
x
,
y
⟩
\lang x,y\rang
⟨x,y⟩ 或
(
x
,
y
)
(x,y)
(x,y)。
Neighbors(G,x)
:列出图
G
G
G 中与结点
x
x
x 邻接的边。
InsertVertex(G,x)
:在图
G
G
G 中插入顶点
x
x
x。
DeleteVertex(G,x)
:从图
G
G
G 中删除顶点
x
x
x。
AddEdge(G,x,y)
:若无向边
(
x
,
y
)
(x,y)
(x,y) 或有向边
⟨
x
,
y
⟩
\lang x,y \rang
⟨x,y⟩ 不存在,则向图
G
G
G 中添加该边。
FirstNeighbor(G,x)
:求图
G
G
G 中顶点
x
x
x 的第一个邻接点,若有则返回顶点号。若
x
x
x 没有邻接点或图中不存在
x
x
x,则返回-1。
NextNeighbour(G,x,y)
:假设图
G
G
G 中顶点
y
y
y 是顶点
x
x
x 的一个邻接点,返回除
y
y
y 之外顶点
x
x
x 的下一个邻接点的顶点号,若
y
y
y 是
x
x
x 的最后一个邻接点,则返回-1。
Get_edge_value(G,x,y)
:获取图
G
G
G 中边
(
x
,
y
)
(x,y)
(x,y) 或
⟨
x
,
y
⟩
\lang x,y\rang
⟨x,y⟩ 对应的权值。
Set_edge_value(G,x,y)
:设置图
G
G
G 中边
(
x
,
y
)
(x,y)
(x,y) 或
⟨
x
,
y
⟩
\lang x,y\rang
⟨x,y⟩ 对应的权值为
v
v
v。
Adjacent(G,x,y)
:判断图
G
G
G 是否存在边
⟨
x
,
y
⟩
\lang x,y\rang
⟨x,y⟩ 或
(
x
,
y
)
(x,y)
(x,y)。
6.3 图的遍历
6.3.1 广度优先遍历(BFS)
要点:
- 找到与一个顶点相邻的所有顶点
FirstNeighbor(G,x)
:求图 G G G 中顶点 x x x 的第一个邻接点,若有则返回顶点号。若 x x x 没有邻接点或图中不存在 x x x,则返回-1。NextNeighbour(G,x,y)
:假设图 G G G 中顶点 y y y 是顶点 x x x 的一个邻接点,返回除 y y y 之外顶点 x x x 的下一个邻接点的顶点号,若 y y y 是 x x x 的最后一个邻接点,则返回-1。
- 标记哪些顶点被访问过
bool visited[MAX_VERTEX_NUM]; //访问标记数组
- 需要一个辅助队列
bool visited[MAX_VERTEX_NUM]; //访问标记数组,初始都为false
//加入了对非连通图的出咯
void BFSTraverse(Graph G) //对图G进行广度优先遍历
{
for(i = 0; i < G.vexnum; ++i)
visited[i] = FALSE; //访问标记数组初始化
InitQueue(Q); //初始化辅助序列Q
for(i = 0; i < G.vexnuml ++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 = NextNeighbour(G, v, w)) //检测v所有邻接点
{
if(!visited[w]) //w为v的尚未访问的邻接顶点
{
visit(w); //访问顶点w
visited[w] = TRUE; //对w做已访问标记
EnQueue(Q, w); //顶点w入队列
}
}
}
}
广度优先生成树
广度优先生成森林
6.3.2 深度优先遍历(DFS)
bool visited[MAX_VERTEX_NUM]; //访问标记数组
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为u的尚未访问的邻接顶点
{
DFS(G, w);
}
}
}
深度优先生成树
(如果沿着某条边找到了结点,就把这条边标红。把红色边留下,就得到了深度优先生成树)
深度优先生成森林
6.3.3图的遍历与图的连通性
- 对无向图进行BFS/DFS遍历,调用BFS/DFS函数的次数=连通分量数
- 对于连通图,只需调用1次BFS/DFS
- 对有向图进行BFS/DFS遍历,调用BFS/DFS函数的次数要具体问题具体分析
- 若起始顶点到其他各顶点都有路径,则只需调用1次BFS/DFS函数
- 对于强连通图,从任一结点出发都只需调用1次BFS/DFS
6.4 图的应用
6.4.1 最小生成树
对于一个带权连通无向图 G = ( V , E ) G=(V,E) G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设 R R R 为 G G G 的所有生成树的集合,若 T T T 为 R R R 中边的权值之和最小的生成树,则 T T T 称为 G G G 的最小生成树(Minimum-Spanning-Tree, MST)
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的
- 最小生成树的边数 = 顶点数 - 1。砍掉一条则不连通,增加一条边则会出现回路
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身
- 只有连通图才有生成树,非连通图只有生成森林
Prim算法(普里姆)
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度: O ( ∣ V ∣ 2 ) O(\left|V \right|^2) O(∣V∣2),适合用于边稠密图
Kruskal算法(克鲁斯卡尔)
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直至所有结点都连通。
时间复杂度: O ( ∣ E ∣ log 2 ∣ E ∣ ) O(\left|E \right| \log_2\left|E \right|) O(∣E∣log2∣E∣),适合用于边稀疏图
6.4.2 最短路径问题(BFS算法)
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G, int u)
{
//d[i]表示从u到i结点的最短路径
for(i = 0; i < G.vexnum; ++i)
{
d[i] = ∞; //初始化路径长度
path[i] = -1; //最短路径从哪个顶点过来
}
d[u] = 0;
visited[u] = TRUE;
EnQueue(Q, u);
while(!isEmpty(Q)) //BFS算法主过程
{
DeQueue(Q, u); //队头元素u出队
for(w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w))
{
if(!visited[w]) //w为u的尚未访问的邻接顶点
{
d[w] = d[u] + 1; //路径长度加1
path[w] = u; //最短路径应从u到w
visited[w] = TRUE; //设已访问标记
EnQueue(Q, w); //顶点w入队
}
}
}
}
总结:
- 就是对BFS的小修改,再visit一个顶点时,修改其最短路径长度d[]并在path[]记录前驱结点。
- BFS算法求单源最短路径只适用于无权图,或所有边的权值都相同的图
6.4.3 最短路径问题(Dijkstra算法)
回顾:
带权路径长度:当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度。
以下将带权路径长度简称为路径长度。
初始:从V0开始,初始化三个数组信息如下:
第1轮:循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i]=true
。检查所有邻接自Vi的顶点,若其final值为false,则更新dist和path信息。
第2轮:循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i]=true
。检查所有邻接自Vi的顶点,若其final值为false,则更新dist和path信息。
结论:Dijkstra算法不适用于有负权值的带权图。
6.4.4 最短路径算法(Floyd算法)
初始:不允许再其他顶点中转,最优路径是?
若允许在V0中转,最短路径是?——求A(0)和path(0)
若允许在V0、V1中转,最短路径是?——求A(1)和path(1)
若允许在V0、V1、V2中转,最短路径是?——求A(2)和path(2)
总结:从A(-1)和path(-1)开始,经过 n n n 轮递推,得到A(n-1)和path(n-1)
//......准备工作,根据图的信息初始化矩阵A和path
for(int k = 0; k < n; k++) //考虑以Vk作为中转点
{
for(int i = 0; i < n; i++) //遍历整个矩阵,i为行号,j为列号
{
for(int j = 0; j < n; j++)
{
if(A[i][j] > A[i][k] + A[k][j]) //以Vk为中转点的路径更短
{
A[i][j] = A[i][k] + A[k][j]; //更新最短路径长度
path[i][j] = k; //中转点
}
}
}
}
结论:
Dijkstra算法适用于**有负权值但是没有负权回路(有负权值的边组成回路)**的的带权图。
总结:
6.4.5 有向无环图(DAG)
有向无环图:若一个有向图中不存在环,则称为有向无环图,简称DAG图(Directed Acyclic Graph)
6.4.6 拓扑排序
AOV网
AOV网(Activity On Vertex Network,用顶点表示活动的网):用有向无环图表示一个工程。顶点表示获得,有向边
⟨
V
i
,
V
j
⟩
\lang V_i,V_j\rang
⟨Vi,Vj⟩ 表示活动
V
i
V_i
Vi 必须先于活动
V
j
V_j
Vj 进行。
拓扑排序
在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑序列:
- 每个顶点出现且只出现一次
- 若顶点 A A A 在序列中排在顶点 B B B 的前面,则在图中不存在从顶点 B B B 到顶点 A A A 的路径。
拓扑排序的实现:
- 从AOV网中选择一个没有前驱(入度为0)的顶点并输出
- 从网中删除该顶点和所有以它为起点的有向边
- 重复1和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止
#define MaxVertexNum 100 //图中顶点数目的最大值
#define VertexType xxx //定义顶点类型
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; //图的顶点数和弧数
}Graph; //Graph是以邻接表存储的图类型
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; //拓扑排序成功
}
逆拓扑排序
逆拓扑排序的实现:
- 从AOV网中选择一个没有后继(出度为0)的顶点并输出
- 从网中删除该顶点和所有以它为终点的有向边
- 重复1和2直到当前的AOV网为空为止
用DFS算法实现逆拓扑排序(区别是只在DFS函数中加了一个print):
bool visited[MAX_VERTEX_NUM]; //访问标记数组
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为u的尚未访问的邻接顶点
{
DFS(G, w);
}
}
print(v); //输出顶点
}
6.4.7 关键路径
AOE网
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge Network)
- AOE网仅有一个入度为0的点,称为开始顶点(源点),它表示整个工程的开始
- AOE网仅有一个出度为0的点,称为结束顶点(汇点),它表示整个工程的结束
性质:
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生
- 有些活动是可以并行进行的
关键路径
从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
活动 a i a_i ai 的时间余量 d ( i ) = l ( i ) − e ( i ) d(i)=l(i)-e(i) d(i)=l(i)−e(i),表示在不增加完成整个工程所需总时间的情况下,活动 a i a_i ai 可以拖延的时间。若一个活动的时间余量为0,则说明该活动必须要如期完成, d ( i ) = 0 d(i)=0 d(i)=0 即 l ( i ) = e ( i ) l(i)=e(i) l(i)=e(i) 的活动 a i a_i ai 是关键活动。由关键活动组成的路径就是关键路径。
求关键路径的步骤
-
求所有事件的最早发生时间 v e ( ) ve() ve()
按拓扑排序序列,依次求各个顶点的 v e ( k ) ve(k) ve(k):
v e ( 源 点 ) = 0 v e ( k ) = Max { v e ( j ) + Weight ( v j , v k ) } , v j 为 v k 的 任 意 前 驱 ve(源点)=0\\ ve(k)=\text{Max}\{ve(j)+\text{Weight}(v_j,v_k) \},v_j为v_k的任意前驱 ve(源点)=0ve(k)=Max{ve(j)+Weight(vj,vk)},vj为vk的任意前驱
-
求所有事件的最迟发生时间 v l ( ) vl() vl()
按逆拓扑排序序列,依次求各个顶点的 v l ( k ) vl(k) vl(k):
v l ( 汇 点 ) = v e ( 汇 点 ) = 0 v l ( k ) = Min { v l ( j ) − Weight ( v k , v j ) } , v j 为 v k 的 任 意 后 继 vl(汇点)=ve(汇点)=0\\ vl(k)=\text{Min}\{vl(j)-\text{Weight}(v_k,v_j) \},v_j为v_k的任意后继 vl(汇点)=ve(汇点)=0vl(k)=Min{vl(j)−Weight(vk,vj)},vj为vk的任意后继
-
求所有活动的最早发生时间 e ( ) e() e()
若边 ⟨ v k , v j ⟩ \lang v_k,v_j\rang ⟨vk,vj⟩表示活动 a i a_i ai,则有 e ( i ) = v e ( k ) e(i)=ve(k) e(i)=ve(k)
-
求所有活动的最迟发生时间 l ( ) l() l()
若边 ⟨ v k , v j ⟩ \lang v_k,v_j\rang ⟨vk,vj⟩表示活动 a i a_i ai,则有 l ( i ) = v l ( j ) − Weight ( v k , v j ) l(i)=vl(j)-\text{Weight}(v_k,v_j) l(i)=vl(j)−Weight(vk,vj)
-
求所有活动的时间余量 d ( l ) d(l) d(l)
d ( i ) = l ( i ) − e ( i ) d(i) = l(i)-e(i) d(i)=l(i)−e(i)
d ( k ) = 0 d(k)=0 d(k)=0的活动就是关键活动( a 2 , a 5 , a 7 a_2,a_5,a_7 a2,a5,a7),由关键活动可得关键路径( V 1 → V 3 → V 4 → V 6 V_1\rightarrow V_3\rightarrow V_4 \rightarrow V_6 V1→V3→V4→V6)。