知识框架
图的基本概念 P207
【课件】6.1.1图的基本概念.pdf
常见考点 无向图 对于 n 个顶点的无向图 G
所有顶点的 若 G 是 连通图 , 则最少有 n-1 条边(树),此时构成一棵树 若 ,则一定有回路 若G是 非连通图 ,则最多可能有 条边 无向完全图 共有 条边
有向图 对于 n 个顶点的有向图 G
所有顶点的 所有顶点的 若 G 是 强连通图 ,则最少有 n 条边(形成一条回路) 有向完全图 共有 条边
图的定义 :::tips
图 _G _由 顶点集 _V _和 边集 _E _组成
记为 $ G = (V, E) $
- 其中 $ V(G) $ 表示图 _G _中顶点的有限非空集
- $ E(G) $ 表示图 _G _中顶点之间的关系(边)集合
若 $ V = \{v_1, v_2, … , v_n\} $ ,则用 $ |V | $ 表示图 _G _中 顶点的个数 ,也称 图 _G _的阶
$ E = {(u, v) | u\in V, v \in V} $ ,用 $ |E| $ 表示图 G 中 边的条数 。
:::
注意: 线性表可以是空表,树可以是空树,但图不可以是空,即 V一定是非空集
连通分量和强连通分量 :::tips 王道书:课后题
P210
12、13、14
:::
:::tips 连通分量:无向图中的极大连通子图
:::
:::tips 强连通分量:有向图中的极大强连通子图
:::
:::tips 连通图的生成树:是包含图中全部顶点的一个极小连通子图
:::
图的存储及基本操作
【课件】6.2.1邻接矩阵法.pdf
【课件】6.2.2邻接表法.pdf
【课件】6.2.3+6.2.4十字链表、邻接多重表.pdf
【课件】6.2.5图的基本操作.pdf
邻接矩阵法 :::info
如何计算指定顶点的度、入度、出度 (分无向图、有向图来考虑)? 时间复杂度如何?
如何找到与顶点相邻的边 (入边、出边)? 时间复杂度如何?
如何存储带权图 ?
空间复杂度 —— $ O(|V|^2) $,适合存储稠密图
无向图的邻接矩阵为对称矩阵 ,如何压缩存储 ?
设图 G 的邻接矩阵为 A (矩阵元素为 0/1 ),则 An 的元素 An [i][j] 等于由顶点 i 到顶点 j 的长度为 n 的路径的数目
:::
基本概念和代码 :::info
空间复杂度: $ O(|V|^2) $—— **只和顶点数相关 **,和实际的边数无关
适合用于 **存储稠密图 **
无向图的邻接矩阵是 **对称矩阵 **,可以 **压缩存储 **(只存储上三角区/下三角区)
:::
1 2 3 4 5 6 7 8 9 10 #define MaxVertexNum 100 typedef char VertexType; typedef int EdgeType; typedef struct { VertexType vex[MaxVertexNum]; EdgeType edge[MaxVertexNum][MaxVertexNum]; int vexnum, arcnum; } MGraph;
:::tips
邻接矩阵法 求顶点的度/出度/入度 的时间复杂度为 O(|V|)
:::
无向图
第 i 个结点的 度 = 第 i 行(或第 i 列) 的非零元素个数
有向图
第 i 个结点的 出度 = 第 i 行 的非零元素个数
第 i 个结点的 入度 = 第 i 列 的非零元素个数
第 i 个结点的 度 = 第 i 行、第 i 列 的非零元素个数之和
矩阵值为无穷:表示两点之间无边
回顾:对称矩阵的压缩存储
邻接矩阵法的性质 :::info
设图 _G _的邻接矩阵为 _**A **_(矩阵元素为 0/1),
则 A n 的元素 $ A^n[i][j] $ 等于由顶点 i 到顶点 j 的长度为 n __ 的 路径的数目
:::
邻接表法 1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct ArcNode { int adjvex; struct ArcNode *nextarc; } ArcNode; typedef struct VNode { VertexType data; ArcNode *firstarc; } VNode, AdjList[MaxVertexNum]; typedef struct { AdjList vertices; int vecnum, arcnum; } ALGraph;
十字链表 —- 有向图 :::info
空间复杂度: O(|V|+|E|)
如何找到指定顶点的所有 **出边 **?——顺着 绿色 线路找
如何找到指定顶点的所有 **入边 **?——顺着 橙色 线路找
注意:十字链表只用于存储 **有向图 **
:::
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710489065742-a2e09c6a-3ce1-4da1-a860-d645e326a8e5.png)
## 邻接多重表 --- 无向图
:::info
1. **空间复杂度 **:O(|V|+|E|)
2. **删除边、删除节点 **等操作很方便
注意:邻接多重表只适 用于存储 **无向图 **
:::
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710489167978-ce603807-3172-4586-8ffe-99f9d06a36e2.png)
## 图的基本操作
:::tips
1. Adjacent(G,x,y):判断图 _G _是否存在边 < _x _, _y _> 或 ( _x _, _y _)
2. Neighbors(G,x):列出图 _G _中与结点 _x _邻接的边
3. InsertVertex(G,x):在图 _G _中插入顶点 _x _
4. DeleteVertex(G,x):从图 _G _中删除顶点 _x _
5. AddEdge(G,x,y):若无向边 ( _x _, _y _) 或有向边 < _x _, _y _> 不存在,则向图 _G _中添加该边
6. RemoveEdge(G,x,y):若无向边 ( _x _, _y _) 或有向边 < _x _, _y _> 存在,则从图 _G _中删除该边
7. FirstNeighbor(G,x):求图 _G _中顶点 _x _的第一个邻接点,
1. 若有则返回顶点号。
2. 若 _x _没有邻接点或图中不存在 _x _,则返回 -1
8. NextNeighbor(G,x,y):假设图 _G _中顶点 _y _是顶点 _x _的一个邻接点,
1. 返回除 _y _之外顶点 _x _的下一 个邻接点的顶点号,
2. 若 _y _是 _x _的最后一个邻接点,则返回 -1
9. Get_edge_value(G,x,y):获取图 _G _中边 ( _x _, _y _) 或 < _x _, _y _> 对应的权值
10. Set_edge_value(G,x,y,v):设置图 _G _中边 ( _x _, _y _) 或 < _x _, _y _> 对应的权值为 _v _
:::
图的遍历
【课件】6.3.1图的广度优先遍历.pdf
【课件】6.3.2图的深度优先遍历.pdf
广度优先搜索
代码及分析 ⼴度优先遍历(Breadth-First-Search, BFS)要点 找到与⼀个顶点相邻的所有顶点 标记哪些顶点被访问过: bool visited[MAXSIZE]
需要⼀个辅助队列 FirstNeighbor(G, x)
:求图 G 中顶点 x 的第⼀个邻接点, 若有则返回顶点号。 若 x 没有邻接点或图中不存在 x,则返回 -1。 NextNeighbor(G, x, y)
:假设图 G 中顶点 y 是顶点 x 的⼀个邻接点,返回除 y 之外 顶点 x 的下⼀个邻接点的顶点号,若 y 是 x 的最后⼀个邻接点,则返回 -1。
:::tips
同⼀个图的 邻接矩阵 表示⽅式 唯⼀ ,因此⼴度优先 遍历序列唯⼀
同⼀个图 邻接表 表示⽅式 不唯⼀ ,因此⼴度优先 遍历序列不唯⼀
:::
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void BFS (ALGraph G, int i) { queue<int > queue; visit (G, i); visited[i] = true ; queue.push (i); while (!queue.empty ()) { int v = queue.front (); queue.pop (); for (int p = FirstNeighbor (G, v); p != -1 ; p = NextNeighbor (G, v, p)) { if (!visited[p]) { visit (G, p); visited[p] = true ; queue.push (p); } } } } void BFSTraverse (ALGraph G) { for (int i = 1 ; i <= G.vecnum; i++) { visited[i] = false ; } for (int i = 1 ; i <= G.vecnum; i++) { if (!visited[i]) { BFS (G, i); } } }
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710503842053-1975bbc1-f5f6-4aa8-869f-946df2eae45c.png)
邻接矩阵 存储的图:
1. 访问 |V| 个顶点需要O(|V|)的时间
2. 查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点
3. 时间复杂度= $ O(|V|^2) $
邻接表 存储的图:
1. 访问 |V| 个顶点需要O(|V|)的时间
2. 查找各个顶点的邻接点共需要O(|E|)的时间,
3. 时间复杂度= $ O(|V|+|E|) $
### 广度优先生成树和森林
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710504044590-0a2680d0-d9c6-4fe4-95e5-0c75767b067d.png)
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710504068600-a7defdfc-3168-42ff-9828-1d535f291a5a.png)
## 深度优先搜索
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710505089042-add44fdd-300b-4854-8185-6a76c2a36f28.png)
### 代码及分析
深度优先遍历(Deapth-First-Search, DFS)要点 类似于树的先根遍历
但需要标记哪些顶点被访问过: bool visited[MAXSIZE]
:::tips
同⼀个图的 邻接矩阵 表示⽅式 唯⼀ ,因此深度优先 遍历序列唯⼀
同⼀个图 邻接表 表示⽅式 不唯⼀ ,因此深度优先 遍历序列不唯⼀
:::
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void DFS (ALGraph G, int i) { visit (G, i); visited[i] = true ; for (int p = FirstNeighbor (G, i); p != -1 ; p = NextNeighbor (G, i, p)) { if (!visited[p]) { DFS (G, p); } } } void DFSTraverse (ALGraph G) { for (int i = 1 ; i <= G.vecnum; i++) { visited[i] = false ; } for (int i = 1 ; i < G.vecnum; i++) { if (!visited[i]) { DFS (G, i); } } }
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710504704608-b1742122-c0de-4d9e-9e1f-de728b773de1.png)
邻接矩阵 存储的图:
1. 访问 |V| 个顶点需要 O(|V|) 的时间
2. 查找每个顶点的邻接点都需要 O(|V|) 的时间,而总共有 |V| 个顶点
3. 时间复杂度= $ O(|V|^2) $
邻接表 存储的图:
1. 访问 |V| 个顶点需要 O(|V|) 的时间
2. 查找各个顶点的邻接点共需要 O(|E|) 的时间,
3. 时间复杂度= $ O(|V|+|E|) $
### 深度优先生成树
:::tips
同⼀个图的 邻接矩阵 表示⽅式 唯⼀ ,因此深度优先 遍历序列唯⼀, 深度优先 ⽣成树也唯⼀
同⼀个图 邻接表 表示⽅式 不唯⼀ ,因此深度优先 遍历序列不唯⼀, 深度优先 ⽣成树也不唯⼀
:::
### 图的遍历与图的连通性
对 无向图 进行 BFS/DFS遍历
+ 调用 BFS/DFS函数的次数 = 连通分量数
+ 对于 连通图 , 只需调用 1次 BFS/DFS
对 有向图 进⾏BFS/DFS遍历
+ 调⽤BFS/DFS函数的次数要具体问题具体分析
- 若起始顶点到其他各顶点都有路径
- 则只需调⽤1次 BFS/DFS 函数
+ 对于 强连通图 ,从任⼀结点出发都只需调⽤1次 BFS/DFS
# 图的应用
[【课件】6.4.1最小生成树.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746603420-46914ed0-03da-4e9c-84e2-5136ae0437ab.pdf)
[【课件】6.4.2_1最短路径问题_BFS算法.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746760168-f8fe6d55-3919-4cf9-9f7b-28ced19a23a0.pdf)
[【课件】6.4.2_2最短路径问题_Dijkstra算法.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746770314-d9bbea61-c085-4350-9448-81183be2167b.pdf)
[【课件】6.4.2_3最短路径问题_Floyd算法.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746785935-c0b0d70c-ee6b-4508-926d-50c1f453ca23.pdf)
[【课件】6.4.3有向无环图描述表达式.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746798508-0cc1c092-c2cb-49d7-bebc-63fb45a436c9.pdf)
[【课件】6.4.4拓扑排序.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746810594-f84c0a24-69e9-4162-8497-1e32141b0920.pdf)
[【课件】6.4.5关键路径.pdf](https://www.yuque.com/attachments/yuque/0/2024/pdf/40548704/1710746828508-b6527e96-24a5-44ff-bc5d-9d2237e46cd6.pdf)
## 最小生成树
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710681676127-2c0f0891-e0ab-4d37-87d4-1f948cbddf17.png)
### 基本概念
🍓 生成树的概念 连通图的 生成树 是包含图中全部顶点的一个 极小连通子图 。
若图中顶点数为 n,则它的生成树含有 n-1 条边。 对生成树而言,若砍去它的一条边,则会变成 非连通图, 若加上一条边则会形成一个 回路 。
🥝 最小生成树(MST) 对于⼀个 带权连通无向图 G = (V, E) ,生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。
设 R 为 G 的所有生成树的集合,若 T 为 R 中 边的权值之和最小的生成树 , 则 T 称为 G 的 最小生成树( Minimum-Spanning-Tree, MST ) 。
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710680652913-40349a17-6f20-41aa-9a8b-2fa8ab4a4024.png)
🍅最小生成树的性质
:::tips
1. **最小生成树**可能有多个 ,但边的权值之和总是唯一且最小 的
2. $ 最小生成树的边数 = 顶点数-1 $ 。
1. 砍掉一条则不连通,增加一条边则会出现回路
3. 如果一个连通图本身就是一棵树 ,则其最小生成树就是它本身
4. 只有连通图才有生成树,非连通图只有生成森林
:::
### Prime 算法(加边加点法)
🍏 Prim 算法(普里姆) 从一个顶点开始构建生成树 每次将代价最小的新顶点纳入生成树 直到所有顶点都纳入为止
:::info
**时间复杂度:**$ O(|V|^2) $
+ 适合边稠密图
:::
🍎 实现思想 从 V₀ 开始,总共需要 n-1 轮 处理
每一轮处理:循环遍历所有个结点,找到 lowCost 最低的,且还没加入树的顶点。 再次循环遍历,更新还没加入的各个顶点的 lowCost 值
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710681381425-0e0971b1-7acf-4614-b729-2b85fa48c96b.png)
### Kruskal 算法(加边法)
🍊 Kruskal 算法(克鲁斯卡尔) 每次选择一条权值最小的边, 使这条边的两头连通(原本已经连通的就不选) 直到所有结点都连通 通常用 堆 来存放边的集合(小根堆)
每次取堆顶元素,并重新构造小根堆( ) 判断边的两个顶点是否是同一集合,使用 并查集 判断(可视为常数级) 最坏情况需要遍历完所有边
:::info
🍇**时间复杂度:**$ O(|E|*log|E|) $
+ 适合边稀疏图
:::
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710681631734-4e319ee9-ce4c-4270-9bfb-6dc72435c721.png)
## 最短路径
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710683415256-00da84af-c1e7-4764-9ce2-4fc395881ae4.png)
### BFS 求单源最短路径
:::tips
BFS 算法求单源最短路径只适⽤于 **无权图 **,或 **所有边的权值都相同的图 **
:::
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710682058391-5ff2ef31-a62b-4ca1-81ff-5fa33aa30d9a.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int d[MaxVertexNum]; int path[MaxVertexNum]; void BFS_Min_Distance (ALGraph G, int u) { queue<int > queue; for (int i = 1 ; i <= MaxVertexNum; i++) { d[i] = 0x3f3f3f3f ; path[i] = -1 ; visited[i] = false ; } d[u] = 0 ; visited[u] = true ; queue.push (u); while (!queue.empty ()) { u = queue.front (); queue.pop (); for (int p = FirstNeighbor (G, u); p >= 0 ; p = NextNeighbor (G, u, p)) { if (!visited[p]) { visited[p] = true ; queue.push (p); d[p] = d[u] + 1 ; path[p] = u; } } } } void PrintXToY (int x, int y) { printf ("%d-->%d最短长度为:%d\n" , x, y, d[y]); int arr[MaxVertexNum]; int count = 0 ; for (int i = y; i >= 0 ; i = path[i]) { arr[count++] = i; } for (int i = count - 1 ; i >= 0 ; i--) { printf ("%d, " , arr[i]); } puts ("" ); }
### Dijkstra 算法求单源最短路径问题
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710682154219-0b5f4216-5b46-4f94-9d20-1d2e65b5f2cd.png)
Dijkstra 算法步骤 初始化:
若从 V 0 开始,令 final[0] = true; dist[0] = 1; path[0] = -1
其余顶点 final[k] = false;
dist[k] = arcs[0][k];
path[k] = (arcs[0][k] == ∞) ? -1 : 0
n-1 轮处理:
循环遍历所有顶点, 找到还没确定最短路径 且 dist 最小的顶点 Vi ,令 final[i] = true
并检查所有邻接自 Vi 的顶点 对于邻接自 Vi 的顶点 Vj ,若 final[j] = false
且 dist[i] + arc[i][j] < dist[j]
则令 dist[j] = dist[i] + arc[i][j]
path[j] = i
其中 arcs[i][j]
表示 Vi 到 Vj 的弧的权值
:::info
时间复杂度: $ O(n^2) $即 $ O(|V|^2) $
结论: Dijkstra 算法不适用于 有负权值 的带权图
注: 也可用 Dijkstra 算法求所有顶点间的最短路径, 重复 |V| 次 即可,总的时间复杂度也是 $ O(|V|^3) $
:::
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 bool final [MaxVertexNum]; int dist[MaxVertexNum]; int path[MaxVertexNum]; void Dijkstra (ALGraph G, int u) { for (int i = 1 ; i <= G.vecnum; i++) { final [i] = false ; dist[i] = 0x3f3f3f3f ; path[i] = -1 ; } final [u] = true ; dist[u] = 0 ; dist[0 ] = 0x3f3f3f3f ; for (ArcNode *p = FirstNeighbor (G, u); p; p = NextNeighbor (G, u, p->adjvex)) { dist[p->adjvex] = p->weight; path[p->adjvex] = u; } int count = 0 ; while (++count < G.vecnum) { int minV = 0 ; for (int i = 1 ; i <= G.vecnum; i++) { if (!final [i] && dist[minV] > dist[i]) { minV = i; } } final [minV] = true ; for (ArcNode *p = FirstNeighbor (G, minV); p; p = NextNeighbor (G, minV, p->adjvex)) { if (!final [p->adjvex]) { if (dist[p->adjvex] > dist[minV] + p->weight) { dist[p->adjvex] = dist[minV] + p->weight; path[p->adjvex] = minV; } } } } } void PrintXToY (int path[], int dist[], int x, int y) { printf ("%d-->%d最短长度为:%d\n" , x, y, dist[y]); int arr[MaxVertexNum]; int count = 0 ; for (int i = y; i >= 0 ; i = path[i]) { arr[count++] = i; } for (int i = count - 1 ; i >= 0 ; i--) { printf ("%d, " , arr[i]); } puts ("" ); }
### Floyd 算法求各顶点之间的最短路径问题
:::info
**Floyd 算法 **
+ 可以解决带负权值的图
+ 但不能解决带有 “负权回路” 的图(有负权值的边组成回路),这种图有可能没有最短路径
:::
Floyd算法:求出每一对顶点之间的最短路径 使用 动态规划 思想,将问题的求解分为多个阶段
对于 n 个顶点的图 G,求任意一对顶点 Vi->Vj
之间的最短路径可分为如下几个阶段:
初始:不允许在其他顶点中转,最短路径是? 若允许在V₀中转,最短路径是? 若允许在Vo、V₁中转,最短路径是? 若允许在Vo、V₁、V₂中转,最短路径是? …… n-1: 若允许在Vo、V₁、V₂……Vn-1中转,最短路径是?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int A[MaxVertexNum][MaxVertexNum];int path[MaxVertexNum][MaxVertexNum];void Floyd (MGraph G) { for (int i = 1 ; i <= G.vexnum; i++) { for (int j = 1 ; j <= G.vexnum; j++) { path[i][j] = -1 ; A[i][j] = G.edge[i][j]; } } for (int k = 1 ; k <= G.vexnum; k++) { for (int i = 1 ; i <= G.vexnum; i++) { for (int j = 1 ; j <= G.vexnum; j++) { if (A[i][k] + A[k][j] < A[i][j]) { A[i][j] = A[i][k] + A[k][j]; path[i][j] = k; } } } } }
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710683263869-013bab13-d15d-4762-89c6-4f24eb36df89.png)
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710683285031-d1360784-64c2-4b00-b1ce-ad48025c56c3.png)
## 有向无环图描述表达式
有向无环图(DAG): 若一个 有向图 中 不存在环 ,则称为 有向无环图 ,简称 DAG图 (Directed Acyclic Graph)
解题方法 Step 1: 把各个 操作数不重复地排成一排 Step 2: 标出各个 运算符的生效顺序 (先后顺序有点出入无所谓) Step 3: 按 顺序加入运算符 ,注意“ 分层 ” Step 4: 从底向上 逐层检查同层的运算符是否可以合体
:::info
注:标出不同的运算符生效顺序,会得到不同的 DAG 图
:::
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710683637020-5c91faff-cb81-4464-b738-e3fe6c837e1b.png)
## 拓扑排序
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710684751490-ba6b3ae9-e6d2-449c-a250-c7823de32366.png)
:::info
:::
### AOV 网
AOV网 (Activity On Vertex Network,用顶点表示活动的网) 用DAG图(有向无环图)表示 一个工程。 顶点表示活动,有向边 <Vi,V>
表示活动 Vi 必须先于活动 Vj 进行
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710684053216-e02547eb-f238-4299-b2b9-3e75e4787d9f.png)
### 拓扑排序
拓扑排序: 在图论中,由一个 有向无环图 的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
每个顶点出现且只出现一次。 若顶点 A 在序列中排在顶点 B 的前面,则在图中不存在从顶点 B 到顶点 A 的路径 或定义为:
拓扑排序是对有向无环图的顶点的一种排序, 它使得若存在一条从顶点 A 到顶点 B 的路径, 则在排序中顶点 B 出现在顶点 A 的后面。 每个AOV网都有 一个或多个拓扑排序序列 。
---
实现思路 从AOV网中选择⼀个没有前驱的顶点并输出。 从网中删除该顶点和所有以它为起点的有向边。 重复 ① 和 ② 直到当前的 AOV 网为空 或 当前网中不存在无前驱的顶点为止 。 说明有回路
:::info
时间复杂度: $ O(|V|+|E|) $
+ 每个顶点需要处理一次
+ 每条边需要处理一次
若采用邻接矩阵 ,则需 $ O(|V|^2) $
:::
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 int print[MaxVertexNum];bool TopoLogicSort (ALGraph G) { int indegree[MaxVertexNum]; stack<int > stack; for (int i = 1 ; i <= G.vexnum; i++) { indegree[i] = 0 ; } for (int i = 1 ; i <= G.vexnum; i++) { for (int p = FirstNeighbor (G, i); p > 0 ; p = NextNeighbor (G, i, p)) { indegree[p]++; } } for (int i = 1 ; i <= G.vexnum; i++) { if (indegree[i] == 0 ) { stack.push (i); } } int count = 1 ; while (!stack.empty ()) { int u = stack.top (); stack.pop (); visit (G, u); print[count++] = u; for (int p = FirstNeighbor (G, u); p > 0 && indegree[p] != 0 ; p = NextNeighbor (G, u, p)) { indegree[p]--; if (indegree[p] == 0 ) { stack.push (p); } } } if (count <= G.vexnum) { return false ; } else return true ; }
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710684475359-b0ee6924-5daa-4271-b2c7-30d89b3eb57f.png)
### 逆拓扑排序(DFS)
逆拓扑排序 对一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
① 从AOV网中选择一个没有后继 ( 出度为 0 ) 的顶点并输出。
② 从网中删除该顶点和所有以它为终点的有向边。
③ 重复①和②直到当前的AOV网为空。
:::info
对于逆拓扑排序 ,可以模仿拓扑排序
+ 只是把入度判断变为出度判断
+ 可使用邻接矩阵
+ 或者使用逆邻接表
:::
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool visited[MaxVertexNum];void DFS (ALGraph G, int u) { visited[u] = true ; for (int p = FirstNeighbor (G, u); p > 0 ; p = NextNeighbor (G, u, p)) { if (!visited[p]) { DFS (G, p); } } printf ("%d, " , u); } void DFSRTopoLogicSort (ALGraph G) { for (int i = 1 ; i <= G.vexnum; i++) { visited[i] = false ; } for (int i = 1 ; i <= G.vexnum; i++) { if (!visited[i]) { DFS (G, i); } } }
## 关键路径
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710746538355-73b2a827-80ed-4f4a-9952-9f013ab8c19a.png)
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710773319780-60220b46-9dc9-4c41-b16f-28a3a25ed6ce.png)
### AOE 网
AOE 网(Activity On Edge NetWork) 在带权有向图中,
以 顶点表示事件 , 以 有向边表示活动 , 以 边上的权值表示完成该活动的开销 (如完成活动所需的时间), 称之为用边表示活动的网络,简称 AOE 网(Activity On Edge NetWork)
:::info
🍎_AOE _ 网具有以下两个性质:
1. 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
2. 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
3. 另外,有些活动是可以并行进行的
:::
:::color1
🍏开始顶点(源点)
+ 在AOE 网中 仅有⼀个 入度为 0 的顶点,称为 开始顶点(源点) ,
+ 它表示整个工程的开始;
🍑结束顶点(汇点)
+ 也 仅有⼀个 出度为0的顶点,称为 结束顶点(汇点) ,
+ 它表示整个工程的结束了。
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710771518297-b27ccd35-1fdb-45a8-9819-cedb7795532b.png)
:::
---
### 关键路径和关键活动
:::danger
🍅关键路径
+ 从源点到汇点的有向路径可能有多条,
+ 所有路径中,具有 最大路径长度的路径 称为 关键路径 ,
🥝关键活动
+ 而把关键路径上的活动称为 关键活动
![](https://cdn.nlark.com/yuque/0/2024/png/40548704/1710771498268-4bc93dc0-55b5-4b39-bbff-cb4854dc3129.png)
1. 完成整个工程的最短时间就是关键路径的长度 ,
2. 若关键活动不能按时完成,则整个工程的完成时间就会延长
:::
缩短工期的相关分析 若关键活动耗时增加,则整个工程的工期将增长缩短关键活动的时间, 可以缩短整个工程的工期 当缩短到一定程度时,关键活动可能会变成 非关键活动 可能有多条关键路径, 只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期, 只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。
:::info 🍊事件 _**<font style="color:rgb(255,147,0);">vk</font>**_
的最早发⽣时间 _**<font style="color:rgb(255,147,0);">ve</font>**_**<font style="color:rgb(255,147,0);">(</font>**_**<font style="color:rgb(255,147,0);">k</font>**_**<font style="color:rgb(255,147,0);">)</font>**
—— 决定了所有从 _**<font style="color:rgb(0,0,0);">vk</font>**_
开始的活动能够开工的最早时间
🍅活动 _**<font style="color:rgb(190,0,0);">ai </font>**_
的最早开始时间 _**<font style="color:rgb(190,0,0);">e</font>**_**<font style="color:rgb(190,0,0);">(</font>**_**<font style="color:rgb(190,0,0);">i</font>**_**<font style="color:rgb(190,0,0);">)</font>**
—— 指该活动弧的起点所表示的事件的最早发生时间
:::
:::color1 🍇事件 _**<font style="color:rgb(112,47,159);">vk</font>**_
的最迟发生时间 _**<font style="color:rgb(112,47,159);">vl</font>**_**<font style="color:rgb(112,47,159);">(</font>**_**<font style="color:rgb(112,47,159);">k</font>**_**<font style="color:rgb(112,47,159);">)</font>**
—— 它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间。
🍏活动 _**<font style="color:rgb(1,113,0);">ai</font>**_
的最迟开始时间 _**<font style="color:rgb(1,113,0);">l</font>**_**<font style="color:rgb(1,113,0);">(</font>**_**<font style="color:rgb(1,113,0);">i</font>**_**<font style="color:rgb(1,113,0);">)</font>**
—— 它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
:::
:::color3 🍎活动 _**<font style="color:rgb(190,0,0);">ai</font>**_
的最早开始时间 _**<font style="color:rgb(190,0,0);">e</font>**_**<font style="color:rgb(190,0,0);">(</font>**_**<font style="color:rgb(190,0,0);">i</font>**_**<font style="color:rgb(190,0,0);">)</font>**
—— 指该活动弧的起点所表示的事件的最早发生时间
🍏活动 _**<font style="color:rgb(1,113,0);">ai</font>**_
的最迟开始时间 _**<font style="color:rgb(1,113,0);">l</font>**_**<font style="color:rgb(1,113,0);">(</font>**_**<font style="color:rgb(1,113,0);">i</font>**_**<font style="color:rgb(1,113,0);">)</font>**
—— 它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。
🌰活动 _**<font style="color:rgb(0,0,0);">ai</font>**_
的 时间余量 _**<font style="color:rgb(190,0,0);">d</font>**_**<font style="color:rgb(190,0,0);">(</font>**_**<font style="color:rgb(190,0,0);">i</font>**_**<font style="color:rgb(190,0,0);">)=</font>**_**<font style="color:rgb(190,0,0);">l</font>**_**<font style="color:rgb(190,0,0);">(</font>**_**<font style="color:rgb(190,0,0);">i</font>**_**<font style="color:rgb(190,0,0);">)-</font>**_**<font style="color:rgb(190,0,0);">e</font>**_**<font style="color:rgb(190,0,0);">(</font>**_**<font style="color:rgb(190,0,0);">i</font>**_**<font style="color:rgb(190,0,0);">)</font>**
,
表示在不增加完成整个工程所需总时间的情况下,活动 `_ai _`可以拖延的时间
若⼀个活动的时间余量为零,则说明该活动必须要如期完成,
_<font style="color:rgb(0,0,0);">d</font>_<font style="color:rgb(0,0,0);">(</font>_<font style="color:rgb(0,0,0);">i</font>_<font style="color:rgb(0,0,0);">)=0</font>
即 _<font style="color:rgb(0,0,0);">l</font>_<font style="color:rgb(0,0,0);">(</font>_<font style="color:rgb(0,0,0);">i</font>_<font style="color:rgb(0,0,0);">) = </font>_<font style="color:rgb(0,0,0);">e</font>_<font style="color:rgb(0,0,0);">(</font>_<font style="color:rgb(0,0,0);">i</font>_<font style="color:rgb(0,0,0);">)</font>
的活动 _<font style="color:rgb(0,0,0);">ai</font>_
是 关键活动
由 关键活动 组成的路径就是 关键路径
:::
求关键路径的步骤 :::tips
求所有事件的最早发生时间 <font style="color:rgb(0,0,0);">ve() </font>
求所有事件的最迟发生时间 <font style="color:rgb(0,0,0);">vl() </font>
求所有活动的最早发生时间 <font style="color:rgb(0,0,0);">e() </font>
求所有活动的最迟发生时间 <font style="color:rgb(0,0,0);">l() </font>
求所有活动的时间余量 <font style="color:rgb(0,0,0);">d()</font>
d(i)=0
的活动就是关键活动,
由关键活动可得关键路径
:::
① 求所有事件的最早发生时间 ve( ) 按 拓扑排序 序列,依次求各个顶点的 ve ( k ):
ve (源点) = 0
, vj
为 vk
的任意前驱
② 求所有事件的最迟发生时间 vl( ) 按 逆拓扑排序 序列,依次求各个顶点的 vl ( k ):
vl (汇点) = ve (汇点)
, vj
为 vk
的任意后继
③ 求所有活动的最早发生时间 e( ) 若边 < vk , vj >
表示活动 ai
,
④ 求所有活动的最迟发生时间 l( ) 若边 < vk , vj >
表⽰活动 ai
,
则有
⑤ 求所有活动的时间余量 d( )