文章目录
六、图
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,...,v_n\} V={v1,v2,...,vn},则用 ∣ V ∣ |V| ∣V∣表示图中顶点个数,也称图的阶。 E = { ( u , v ) , v ∈ V , v ∈ V } E = \{(u,v),v\in V,v\in V\} E={(u,v),v∈V,v∈V},用 ∣ E ∣ |E| ∣E∣表示图中边的条数。
线性表可以是空表,树可以是空树,但图不能是空图。
图的顶点集 V V V一定非空,但边集 E E E可以为空。
- 有向图。若 E E E是有向边(弧)的有限集合时,则称图为有向图。弧是顶点的有序对,记为 < v , w > <v,w> <v,w>,其中 v v v称为弧尾, w w w称为弧头。顶点 v v v到顶点 w w w的弧,也称 v v v邻接到 w w w,或 w w w邻接自 v v v。
- 无向图。若 E E E是无向边的有限集合时,则图为无向图。边是顶点的无序对,记为 ( v , w ) (v,w) (v,w)或 ( w , v ) (w,v) (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相关联。
- 简单图。一个图若满足:①不存在重复边。②不存在自身到自身的边。则称图为简单图。
- 多重图。若图中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则称图为多重图。多重图和简单图的定义是相对的。
- 完全图。对于无向图, ∣ E ∣ |E| ∣E∣的取值为 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2的图称为完全图。完全图中任意两个顶点之间都存在边。对于有向图, ∣ E ∣ |E| ∣E∣的取值为 n ( n − 1 ) n(n-1) n(n−1)的有向图称为有向完全图,有向完全图的任意两个顶点之间都存在方向相反的两条弧。
- 子图。两个图 G = ( V , E ) G=(V,E) G=(V,E)和 G ′ = ( V ′ , E ′ ) G'=(V',E') G′=(V′,E′),若 V ′ V' V′是 V V V的子集,且 E ′ E' E′是 E E E的子集,则称 G ′ G' G′是 G G G的子图。(并非所有的子集都能构成子图,这样的子集可能不是图。)若有满足 V ( G ′ ) = V ( G ) V(G')=V(G) V(G′)=V(G)的子图 G ′ G' G′,则成 G ′ G' G′为 G G G的生成子图。
- 连通,连通图,连通分量。若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通的。若途中任意两个顶点都是连通的,则称图为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。(极大连通子图称为极大是因为此时加入任何一个不在图点集中的点都会使它不连通。)
- 强连通图,强连通分量。有向图中,若从顶点 v 到顶点 w 之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为强连通分量。(一般在无向图中讨论连通性,在有向图中讨论强连通性。)
- 生成树,生成森林。连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为 n,则它的生成树含有 n-1 条边。在非连通图中,强连通分量的生成树构成了非连通图的生成森林。(包含无向图全部顶点的极小强连通子图只有生成树满足条件,因为砍去任一条边,图都不再连通。)
- 顶点的度。对于无向图,顶点 v 的度是指依附于该顶点的的边的条数,记为 T D ( v ) TD(v) TD(v)。无向图全部顶点的度的和等于边数的2倍。对于有向图,入度是以顶点 v 为终点的有向边的数目,记为 I D ( v ) ID(v) ID(v),出度是以顶点 v 为起点的有向边的数目,记为 O D ( v ) OD(v) OD(v)。顶点 v 的度等于其入度和出度之和,即 T D ( v ) = I D ( v ) + O D ( v ) TD(v) = ID(v) + OD(v) TD(v)=ID(v)+OD(v)。
- 边的权,网。每条边都可以标上具有某种含义的数值,该数值称为该边的权值。边上带有权值的图称为带权图,也称网。
- 稠密图,稀疏图。边数很少的图称为稀疏图,反之称为稠密图。一般当图满足 ∣ E ∣ < ∣ V ∣ l o g ∣ V ∣ |E|<|V|log|V| ∣E∣<∣V∣log∣V∣时,可以将其视为稀疏图。
- 路径,路径长度,回路。顶点 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},...,v_{i_m},v_q vp,vi1,vi2,...,vim,vq ,关联的边可以理解为路径的构成要素,路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。
- 简单路径,简单回路。路径序列中,顶点不重复的路径称为简单路径,同理除第一个和最后一个顶点外,其余顶点不重复存在的回路称为简单回路。
- 距离。从顶点 u 出发到顶点 v 的最短路径若存在,则此路径的长度称为从 u 到 v 的距离。若从 u 到 v 不存在路径,则记该距离为无穷。
- 有向树。一个顶点入度为 0,其余顶点入度均为 1 的有向图,称为有向树。
6.1.2 图的存储
图的存储必须要完整、准确地反映顶点和边集的信息。
根据不同图的结构和算法,采用不同的存储方式将对程序的效率产生相当大的影响。
6.1.2.1 邻接矩阵
定义:
#define MaxVertexNum 100 // 顶点数目最大值
typedef char VertexType; // 顶点数据类型
typedef int EdgeType;
typedef struct {
VertexType Vex[MaxVertexNum]; // 顶点表
EdgeType Edge[MaxVertexNum][MaxVertexNum];
int vexnum, arcnum; // 顶点数和弧数
} MGraph;
注意:
- 无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。
- 邻接矩阵中的元素仅表示相应的边是否存在时, E d g e T y p e EdgeType EdgeType 可以定义为值为0和1的枚举类型。
邻接矩阵的特点:
- 有向图邻接矩阵的第 i i i 行(列)非零元素的个数正好是第 i i i 个顶点的出度(入度)。
- 确定两点之间是否有边很容易,要确定有多少条边的代价很大。
- 稠密图适合邻接矩阵。
- 图 G G G 的邻接矩阵为 A A A , A n A^n An 的元素等于由顶点 i i i 到顶点 j j j 的长度为 n n n 的路径数目。
6.1.2.2 邻接表
对图中每个顶点建立一个单链表,表示依附于该顶点的边。
定义:
#define MaxVertexNum 100
typedef struct ArcNode{ // 边表结点
int adjvex;
struct ArcNode *next;
}ArcNode;
typedef struct VNode{ // 顶点表结点
VertexType data;
ArcNode *first;
} VNode, AdjList[MaxVertexNum];
typedef struct{ // 邻接表
AdjList vertices;
int vexnum, arcnum; // 顶点数 弧数
} ALGraph;
特点:
- 无向图所需的存储空间为 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V|+2|E|) O(∣V∣+2∣E∣)。有向图所需的存储空间为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。
- 稀疏图适合邻接表。
- 邻接表中,给定一个顶点,可以很快找到它的邻边。但,找到两点之间是否存在边很困难。
- 计算出度方便。
- 表示不唯一,因为个边结点的链接顺序是任意的。
6.1.2.3 十字链表
十字链表是有向图的一种链式存储结构。
十字链表中,对应于有向图中的每条弧有一个结点,对应于每一个顶点也有一个结点。
弧结点: < t a i l v e x , h e a d v e x , h l i n k , t l i n k , i n f o > <tailvex, headvex, hlink, tlink, info> <tailvex,headvex,hlink,tlink,info>。有5个域, t a i l v e x tailvex tailvex (尾域)和 h e a d v e x headvex headvex (头域),分别指示弧尾和弧头两个顶点在图中的位置。链域 h l i n k hlink hlink 指向弧相同的下一条弧,链域 t l i n k tlink tlink 指向弧尾相同的下一条弧。 i n f o info info 指向该弧的相关信息。
顶点结点: < d a t a , f i r s t i n , f i r s t o u t > <data, firstin,firstout> <data,firstin,firstout>。3个域,data存放相关信息, f i r s t i n firstin firstin 和 f i r s t o u t firstout firstout 两个域分别指向以该顶点为弧头或弧尾的第一个弧结点。
十字链表表示法,顶点结点之间是顺序存储的。
图的十字链表表示是不唯一的,一个十字链表只能表示一个图。
6.1.2.4 邻接多重表
无向图的一种链式存储结构。
每条边的表示: < m a r k , i v e x , i l i n k , j v e x , j l i n k , i n f o > <mark,ivex,ilink,jvex,jlink,info> <mark,ivex,ilink,jvex,jlink,info>。mark 为标志域,标记该边是否被搜索过。 i v e x ivex ivex 和 j v e x jvex jvex 为该边依附的两个顶点在图中的位置。 i l i n k ilink ilink 指向下一条依附于顶点 i v e x ivex ivex 的边。 j l i n k jlink jlink 指向下一条依附于顶点 j v e x jvex jvex 的边。info指向和边相关的各种信息。
每个顶点: < d a t a , f i r s t e d g e > <data,firstedge> <data,firstedge>。data存储该顶点的相关信息, f i r s t e d g e firstedge firstedge 域指示第一条依附于该结点的边。
邻接多重表中,所有依附于同一顶点的边串联在同一链表中,每个边结点同时链接在两个链表中。
对于无向图而言,其邻接多重表和邻接表的差别仅在于同一条边在邻接表中用两个结点表示,邻接多重表中仅一个结点。
6.1.3 图的基本操作
独立于图的存储结构,抽象地考虑,忽略各变量的类型。
Adjacent(G, x, y); // 判断图G中是否存在边<x, y>
Neighbors(G, x); // 列出图G中与结点x邻接的边
InsertVertex(G, x); // 在图G中插入顶点x
DeleteVertex(G, x);
AddEdge(G, x, y); // 在图G中添加边<x, y> - 若不存在
RemoveEdge(G, x, y);
FirstNeighbor(G, x); // 图G中顶点x的第一个邻接点
NextNeighbor(G, x, y); // 图G中顶点x的除y以外下一个邻接点
Get_edge_value(G, x, y);
Set_edge_value(G, x, y);
6.2 图的遍历
指从图中的某一顶点出发,按照某种搜索方法沿着图中的所有顶点访问一次且仅访问一次。在访问过程中,必须记下已经访问过的节点以避免重复访问。
6.2.1 广度优先搜索 BFS
一种分层查找的过程。每向前走一步可能访问一批顶点。
不是递归的,需要借助辅助队列。
int visited[MAX_VERTEX_NUM];
void BFS(Graph G, int v){
// visit(v);
visited[v] = 1;
EnQueue(Q, v);
while(!isEmpty(Q)){
DeQueue(Q, v);
int w;
for(w = FirstNeighbor(G, v); w >=0; w = NextNeighbor(G, v, w)){
if(!visited[w]){
visit(w);
visited[w] = 1;
EnQueue(Q, w);
}
}
}
}
void BFSTraverse(Graph G){
int i;
for(i = 0;i < G.vexnum;++i)
visited[i] = 0;
Queue Q;
InitQueue(Q);
for(i = 0;i < G.vexnum;++i)
if(!visited[i])
BFS(G, i);
}
邻接表存储的图中遍历的复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。
邻接矩阵的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
可以解决单源最短路径问题。(非带权图)
int d[MAX_VERTEX_NUM];
int visited[MAX_VERTEX_NUM];
void BFS_MIN_Distance(Graph G, int u){
int i;
for(i = 0;i < G.vexnum;++i){
d[i] = Infinity;
}
visited[u] = 1;
d[u] = 0;
while(!isEmpty(Q)){
DeQueue(Q, u);
int w;
for(w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)){
if(!visited[w]){
visited[w] = 1;
d[w] = d[u] + 1;
EnQueue(Q, w);
}
}
}
}
广度优先搜索的过程,可以得到一棵遍历树,称为广度优先生成树。
一给定图的邻接矩阵存储表示是唯一的,因此广度优先生成树也是唯一的;而邻接表是不唯一的。
6.2.2 深度优先搜索 DFS
递归形式代码。
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
int v;
for(v = 0;v < G.vexnum;++v)
visited[v] = 0;
for(v = 0;v < G.vexnum;++v)
if(!visited[v]) DFS(G, v);
}
void DFS(Graph G, int v){
// visit(v);
visited[v] = 1;
int w;
for(w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)){
if(!visited[w]) DFS(G, w);
}
}
非递归实现:
bool visited[MAX_VERTEX_NUM];
void DFS_Non_RC(Graph G, int v){
int w, i;
Stack S;
InitStack(S);
for(i = 0;i < G.vexnum;++i){
visited[i] = 0;
}
Push(S, v);
visited[v] = 1;
while(!isEmpty(S)){
k = Pop(S);
// visit(k);
for(w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w)){
if(!visited[w]){
Push(S, w);
visited[w] = 1;
}
}
}
}
空间复杂度为 O ( ∣ V ∣ ) O(|V|) O(∣V∣)。
时间复杂度性质与BFS相同。
DFS也可产生一棵深度优先生成树,非连通图产生深度优先生成森林。
6.3 图的应用
6.3.1 最小生成树
一棵生成树包含图的所有顶点,并且只含有尽可能少的边。对于生成树来说,若砍去一条边则会使生成树变成非连通图,若添加一条边,则会形成图中的一条回路。
最小生成树的性质:
- 最小生成树不是唯一的,即树形不是唯一的。当图中各边权值互不相等时,则生成树是唯一。
- 最小生成树的边权值和是唯一的,而且是最小的。
- 边数为顶点数 - 1。
通用最小生成树算法:
GENERIC_MST(G){
T = NULL
while T not a MST
do find the min (u, v) into T and no circle
T = T + (u, v)
}
最关键的性质:假设 G = ( V , E ) G = (V,E) G=(V,E) 是一个带权连通无向图,U 是顶点集 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) 的最小生成树。
6.3.1.1 Prim算法
算法步骤:
- 假设 G = ( V , E ) G = (V,E) G=(V,E) 是连通图,其最小生成树 T = ( U , E T ) T = (U,E_T) T=(U,ET), E T E_T 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 = \empty U={u0},ET=∅。
- 循环:直到 U = V U = V U=V。从图 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,置 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)}。
朴素实现的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2),不依赖于 ∣ E ∣ |E| ∣E∣。适用于求解边稠密的图。
也可改进算法,但增加了实现的时间复杂性。
6.3.1.2 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 = \empty U=V,ET=∅。即每个顶点构成一棵独立的树,T 此时是一个仅含有 ∣ V ∣ |V| ∣V∣ 个顶点的森林。
- 循环:直到 T 是一棵树。按 G 的边的权值递增顺序依次从 E − E T E - E_T E−ET 中选择一条边,若加入这条边后 T 不构成回路,则将其加入 E T E_T ET,否则舍弃。直到 E T E_T ET 中含有 n-1 条边。
时间复杂度为 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(∣E∣log∣E∣)。
适合于边稀疏而顶点较多的图。
6.3.2 最短路径
6.3.2.1 Dijkstra算法
假设从 0 点出发。arcs 表示有向图的邻接矩阵。path[] 表示从源点到当前顶点之间最短路径的前驱节点。
算法步骤:
- 初始化:集合 S 初始为 { 0 } \{0\} {0}, d i s t [ ] dist[] dist[] 初始值为 d i s t [ i ] = a r c s [ 0 ] [ i ] , i = 1 , 2 , . . . , n − 1 dist[i] = arcs[0][i],i = 1,2,...,n-1 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 [ j ] ∣ v i ∈ V − S } dist[j] = Min\{dist[j]|v_i\in V-S\} dist[j]=Min{dist[j]∣vi∈V−S}。令 S = S ∪ { j } S = S\cup \{j\} S=S∪{j}。
- 修改集合 V − S V-S V−S 上任一顶点 v k v_k vk 可达的最短路径长度:若 d i s t [ j ] + a r c s [ j ] [ k ] < d i s t [ k ] dist[j] + arcs[j][k]<dist[k] dist[j]+arcs[j][k]<dist[k],则修改 d i s t [ k ] dist[k] dist[k]。
- 重复操作共 n-1 次,直到所有的顶点都包含在 S 中。
边上有负权,将不再使用。
时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)。
6.3.2.2 Floyd算法
递推生成一个方阵序列 A ( − 1 ) , A ( 0 ) , A ( 1 ) . . . , A ( n − 1 ) A^{(-1)},A^{(0)},A^{(1)}...,A^{(n-1)} A(−1),A(0),A(1)...,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 个顶点的运算步骤。
算法步骤:
- 初始化:方阵 A ( − 1 ) [ i ] [ j ] = a r c s [ i ] [ j ] A^{(-1)}[i][j] = arcs[i][j] A(−1)[i][j]=arcs[i][j]。
- 将 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 0 A^0 A0。
- 对其余顶点进行该操作,直到得到 A ( n − 1 ) A^{(n-1)} A(n−1)。
时间复杂度为 O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)。
允许有负权,但不允许有负环。
也使用与无向图。
6.3.2.4 拓扑排序 - AOV网
有向无环图DAG。可以用来描述表达式,比二叉树更节省空间。
AOV网:若用DAG图表示一个工程,其顶点表示活动,用有向边 < V i , V j > <V_i,V_j> <Vi,Vj> 表示活动 V i V_i Vi 必需先于活动 V j V_j Vj 进行的这样一种关系,则将这种有向图称为定点表示活动的网络,记为AOV网。这种前驱和后继关系具有传递性,且任何活动不能以它本身作为自己的前驱或后继。
拓扑排序:一个有向无环图的顶点组成的序列,当且仅当满足下列条件是,称为该图的一个拓扑序
- 每个顶点出现且进出现一次。
- 若顶点 A 在序列中排在顶点 B 的前面,则在图中不存在从顶点 B 到顶点 A 的路径。
或定义为,拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点 A 到顶点 B 的路径,则在排序中顶点 B 出现在顶点 A 的后面。
每个AOV网都有一个或多个拓扑排序序列。
求AOV网的步骤:
- 从AOV网中选择一个没有前驱的顶点并输出。
- 从网中删除该顶点和所有以它为起点的有向边。
- 重复。直到AOV网为空或不存在无前驱的点。
算法实现:
int indegree[MAX_VERNUM];
int TopologicalSort(Graph G){
Stack S;
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.vertics[i].firstarc;p;p = p->nextarc){
v = p->adjvex;
if(!(--indegree[v])){
Push(S, v);
}
}
}
if(count < G.vexnum) return 0;
return 1;
}
时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)。
拓扑排序的结果通常不唯一。但若各个顶点已经排在一个线性有序的序列中,每个顶点有唯一的前后继关系,则结果唯一。
如果按拓扑排序的结果重新编号,生成邻接矩阵,这种邻接矩阵可以是三角阵。三角阵一定存在拓扑序。
DFS 也可得到拓扑序。
int visited[MAX_VERTEX_NUM];
int time; // 设置截止时间
int finishTime[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
int v;
for(v = 0;v < G.vexnum;++v){
visited[v] = 0;
}
time = 0;
for(v = 0;v < G.vexnum;++v){
if(!visited[v]) DFS(G, v);
}
}
void DFS(Graph G, int v){
int w;
visited[v] = 1;
// visit(v);
for(w = FirstNeighbor(G, v); w >= 0;w = NextNeighbor(G, v, w)){
if(!visited[w]) DFS(G, w);
}
time = time + 1;
finishTime[v] = time;
}
// 将序列的结束时间按照从小到大排序即可得到拓扑序
逆拓扑排序:
- 从AOV网中选择一个没有后继的顶点并输出。
- 从网中删除该顶点和所有以它为终点的有向边。
- 重复。直到当前的AOV网为空。
6.3.2.5 关键路径 - AOE网
在带权有向图中,以顶点表示时间,以有向边表示活动,以边上的权值表示完成该活动的开销,称之为用边表示活动的网络,简称 AOE 网。AOE 网和 AOV 网都是有向无环图,不同之出在于他们的边和顶点所代表的含义是不同的,AOE 网中的边有权值,而 AOV 网中的边无权值,仅表示顶点之间的前后关系。
AOE 网的性质:
- 只有在某顶点所表示的时间发生后,从该顶点出发的各有向边所代表的活动才能开始。
- 只有在进入某定点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
在 AOE 网中仅有一个入度为 0 的顶点,称为开始顶点(源点),它表示这个工程的开始。网中也存在一个出度为 0 的点,称为结束顶点(汇点),它表示整个工程的结束。
在 AOE 网中,有些活动是可以并行进行的。从源点到汇点的有向路径可能有多条,并且这些路径长度可能不同。完成不同的路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。
关键活动影响了整个工程的时间,即若关键活动不能按时完成,整个工程的完成时间就会延长。
几个参变量:
-
事件 v k v_k vk 的最早发生时间 v e ( k ) ve(k) ve(k)。
指从源点 v 1 v_1 v1 到顶点 v k v_k vk 的最长路径长度。
计算的递推式:
- v e ( s r c ) = 0 ve(src) = 0 ve(src)=0。
- v e ( k ) = M a x ( v e ( j ) + W e i g h t ( v j , v k ) ) ve(k) = Max(ve(j)+Weight(v_j,v_k)) ve(k)=Max(ve(j)+Weight(vj,vk)), v k v_k vk 为 v j v_j vj 的任意后继。
计算 v e ve ve 值时,按从前往后的顺序计算,可以在拓扑排序的基础上计算:
- 初始时,令 v e [ 1... n ] = 0 ve[1...n] = 0 ve[1...n]=0。
- 输出一个入度为 0 的顶点 v j v_j vj,计算它所有直接后继顶点 v k v_k vk 的最早发生时间。 v k = m a x ( v k , v j + W e i g h t ( v j , v k ) ) v_k = max(v_k,v_j+Weight(v_j,v_k)) vk=max(vk,vj+Weight(vj,vk))。
- 直至输出所有顶点。
-
事件 v k v_k vk 的最迟发生时间 v l ( k ) vl(k) vl(k)。
指在不推迟整个工程完成的前提下,即保证它的后继事件 v j v_j vj 在其最迟发生时间 v l ( j ) vl(j) vl(j) 能够发生时,该事件最迟必须发生的时间。计
算的递推式:
- v l ( s i n k ) = v e ( s i n k ) vl(sink) = ve(sink) vl(sink)=ve(sink)(汇点)。
- v l ( k ) = M i n ( v l ( j ) − W e i g h t ( v k , v j ) ) vl(k) = Min(vl(j) - Weight(v_k,v_j)) vl(k)=Min(vl(j)−Weight(vk,vj)), v k v_k vk 为 v j v_j vj 的任意前驱。
可以在逆拓扑排序的基础上计算,需增设一个栈,拓扑排序结束后栈中记录逆拓扑有序序列。
过程如下:
- 初始时,令 v l [ 1... n ] = v e [ 1... n ] vl[1...n] = ve[1...n] vl[1...n]=ve[1...n]。
- 栈顶顶点 v j v_j vj 出栈,计算其所有直接前驱顶点 v k v_k vk 的最迟发生时间, v l k = m i n ( v l k , v l j − W e i g h t ( v k , v j ) ) vl_k=min(vlk,vl_j-Weight(v_k,v_j)) vlk=min(vlk,vlj−Weight(vk,vj))。
- 直至栈为空。
-
活动 a i a_i ai 的最早开始时间 e ( i ) e(i) e(i)。指该活动弧的起点所表示的事件的最早发生时间。若边 < v k , v j > <v_k,v_j> <vk,vj> 表示活动 a i a_i ai,则有 e ( i ) = v e ( k ) e(i) = ve(k) e(i)=ve(k)。
-
活动 a i a_i ai 的最迟开始时间 l ( i ) l(i) l(i)。 l ( i ) = v l ( j ) − W e i g h t ( v k , v j ) l(i) = vl(j)-Weight(v_k,v_j) l(i)=vl(j)−Weight(vk,vj)。
-
一个活动 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)。指该活动完成时间的余量。 d ( i ) = 0 d(i)=0 d(i)=0 的活动是关键活动。
求关键路径的算法步骤:
- 从源点出发,令 v e ( s r c ) = 0 ve(src) = 0 ve(src)=0,按拓扑有序求其顶点的最早发生时间 v e ( ) ve() ve()。
- 从汇点出发,令 v l ( s i n k ) = v e ( s i n k ) vl(sink) = ve(sink) vl(sink)=ve(sink),按逆拓扑有序求其余顶点的最迟发生时间 v l ( ) vl() vl()。
- 根据各顶点的 v e ve ve 值求所有弧的最早开始时间 e ( ) e() e()。
- 根据个顶点的 v l vl vl 值求所有弧的最迟开始时间 l ( ) l() l()。
- 求 AOE 网中所有活动的差额 d ( ) d() d(),找出所有 d ( ) = 0 d() = 0 d()=0 的活动构成关键路径。
对于关键路径,需要注意:
- 关键路径上的所有活动都是关键活动,是决定整个工程的关键因素。
- 可以通过加快关键活动来缩短整个工程的工期。但,缩短到一定程度,关键活动可能会变化。
- 网中的关键路径不唯一,且对于几条关键路径的网,只提高一条关键路径上的关键活动不能缩短整个工期。
6.4 examples
e.1
判断一棵无向图是否是树。
G 必需是无回路的连通图或有 n-1 条边的连通图。二者均可判断。
- 无回路判断 + 连通性判断。
- n 个顶点 n-1 条边 + 连通性判断。
e.2
用Dijkstra算法得到的生成树是否为最小生成树。
否。
同 Prim 相比,在添加路径时,Dijkstra 基于当前点到 s r c src src 的总路径。而 Prim 仅基于这一条路径的长度。所以不一定是最小生成树。