NO.10数据结构图|Prim算法|Kruskal算法|Dijkstra算法|Floyd算法|拓扑排序|关键路径
图的基本应用
最小生成树
定义
生成树: 生成树是针对无向连通图而言的, 如果一棵树包含一个无向连通图的所有顶点, 那么我们把它叫做这个图的生成树。 (如我们之前提到的深度优先生成树和 广度优先生成树)
生成树有以下性质:
- 这棵树是一个连通图。
- 如果去掉这棵树的一条边, 它会变成非连通图。
- 如果增加一条边, 会形成图中的一条回路。
生成树示意图:
最小生成树: 对于一个带权连通无向图 G = (V, E), 生成树不同, 每棵树的权(即树中所有边上的权值之和) 也可能不同, 我们把权值最小的那棵树叫做该图的最小生成树。
如图, 生成树 1 和生成树 2 都是最小生成树。
由上图我们可以知道, 最小生成树不唯一, 但权值唯一。
当图中各边权值互不相等 --> 其生成树唯一(反之不成立) 。
(因为图当中权值相等的边不一定会变成最小生成树的一部分)
通常我们可以用两种方法得到最小生成树: Prim 算法和 Kruskal 算法
Prim 算法
Prim 算法构造最小生成树的算法思路是:
- 维护一个集合 s, 初始为空;
- 从图中任选一点, 加入集合 s;
- 如果图中所有结点都已经在集合 s 中, 则算法结束, 集合中的点和边共同组成该图的最小生成树; 否则, 进入第 4 步;
- 从图中选择一个到当前集合 s 中的结点距离最近的结点, 连同这条边加入到集合 s 中, 到第 3 步。
算法过程示意
算法时间复杂度
每个轮次都需要从图中选择一个到当前集合 s 中的结点距离最近的结点,需要进行|V|个轮次, 所以 Prim 算法的时间复杂度为 O(|V|^2), 不依赖于|E|, (其中|V|指图中顶点的个数, |E|指图中边的个数) , 所以该算法适用于求解边稠密的图的最小生成树。
Kruskal 算法(克鲁斯卡尔算法)
Kruskal 算法构造最小生成树的思路是:
- 维护一个集合 s, 将图中所有顶点加入集合 s。
- 集合中的点和边会构成若干个连通分量(初始时, 每个顶点都是一个单独的连通分量) 。
- 若集合中只有一个连通分量, 则算法终止, 该连通分量就是这个图的生成树; 否则到第 4 步。
- 从当前未选取过的边中, 选取权值最小、 两个端点(顶点) 落在集合 s 中不同的连通分量上的一条边, 加入集合 s。 转到第 3 步。
算法过程示意
算法时间复杂度
Kruskal 算法的时间复杂度为 O(|E|log|E|), 依赖于|E|而非|V|(其中, |V|指图中顶点的个数, |E|指图中边的个数) , 所以 Kruskal 算法适合边稀疏而顶点较多的图。
Dijkstra 算法(迪杰斯特拉算法)
最短路径
- 带权路径长度:
带权图中, 从一个顶点到另一个顶点所经历的边的权值之和, 叫做该路径的带权路径长度。 - 最短路径:
从一个顶点到另一个顶点可能不止一条路径, 带权路径长度最小的那条叫做最短路径。
定义
单源最短路径问题: 求图中某一顶点到其他各顶点的最短路径。
求解单源最短路径问题通常采用 Dijkstra 算法。
算法思路
Dijkstra 算法思路如下(顶点总数为 n) :
- 维护两个数组:
dist[]
:dist[i]
表示源点到顶点 i 的当前已知的最短路径长度。 初始时, 将dist[]
的每个元素值设为∞ (源点处为 0)。
path[]
:path[i]
表示从源点到顶点 i 的最短路径上, 顶点 i 的前驱顶点。 初始均为-1.
算法思路如下(顶点总数为 n) :
- 集合 s 中存放已经求得最短路径的顶点, 初始为空, 然后将源点加入集合 s;
- 如果所有顶点都加入了集合 s, 则算法结束; 否则, 当前加入集合 s 的顶点中, 最后一个加入的是顶点 u, 对于每一个
dist[v]
(v = 1, 2, …, n 且未加入集合 s) 如果 dist[u]+∣<u,v>∣<dist[v]dist[u]+|<u,v>| < dist[v]dist[u]+∣<u,v>∣<dist[v], 则令 dist[v]=dist[u]+∣<u,v>∣dist[v] =dist[u] + |<u,v>|dist[v]=dist[u]+∣<u,v>∣, path[v]=upath[v] = upath[v]=u。 - 选取
{dist[v] | v = 1, 2, ..., n)}
中的最小值, 将对应的顶点 v 加入集合 s, 跳转到步骤 2。
算法伪代码
//G为图
//数组dist[i]表示源点a到顶点i的当前已知的最短路径长度。
//数组path[i]表示从源点到顶点i的最短路径上,顶点i的前驱顶点。初始均为-1。
//a为源点
Dijkstra(G, dist[], path[],a){初始化dist[],将dist[]的每个元素值设为oo(源点处为0);初始化path[],将path[]的每个元素值设为-1; for(循环n次){u = 使dist[u]最小的还未被访问的顶点的标号;记u已被访问过;for(从u出发能到达的所有顶点v){if(v未被访问 && 以u为中介点使顶点a到达顶点v的最短距离dist[v]更优){更新dist[v];path[v] = u; //更新path[v]}}}
}
算法复杂度
Dijkstra 算法的时间复杂度为 O(|V|^2) (|V|为图中顶点的个数)
注意: Dijkstra 算法不适用于有负权值边的图。
弗洛依德(Floyd)算法
求解图中各个顶点之间的最短路径问题
核心思想:递推求解 n(顶点个数)阶方阵序列 A(−1),A(0),A(1),…,A(n−1)A^{(-1)},A^{(0)},A^{(1)},\dots,A^{(n-1)}A(−1),A(0),A(1),…,A(n−1), 其中A(−1)A^{(-1)}A(−1)表示原图的邻接矩阵。 不断地通过绕行 VKV_{K}VK顶点去计算当前路径的路径长度。
算法步骤:
- 初始化矩阵A(−1)A^{(-1)}A(−1) ;
- 求解A(0)A^{(0)}A(0), 将V0V_{0}V0作为中间点,对于所有的顶点对 {i,j}\{i,j\}{i,j},计算dist(i,v0)+dist(v0,j)dist(i,v_{0})+dist(v_{0}, j)dist(i,v0)+dist(v0,j)
(A(−1)[i][v0]+A(−1)[v0][j]A^{(-1)}[i][v_{0}]+A^{(-1)}[v_{0}][j]A(−1)[i][v0]+A(−1)[v0][j]),若小于 A(−1)[i][j]A^{(-1)} [i][j]A(−1)[i][j],则更新其值,否则不更新; - 对于A(K)A^{(K)}A(K), 即将VKV_{K}VK作为中间点,对于所有的顶点对{i,j}\{i,j\}{i,j} , 计 算dist(i,vK)+dist(vK,j)dist(i,v_{K})+dist(v_{K}, j)dist(i,vK)+dist(vK,j)
(A(−1)[i][vK]+A(−1)[vK][j]A^{(-1)}[i][v_{K}]+A^{(-1)}[v_{K}][j]A(−1)[i][vK]+A(−1)[vK][j]),若小于 A(K−1)[i][j]A^{(K-1)} [i][j]A(K−1)[i][j],则更新其值,否则不更新; - 计算完 A(n−1)A^{(n-1)}A(n−1),算法结束。
注: 在求解 A(K)A^{(K)}A(K)的时候, 第 k 行/列, 主对角线是不会改变的
弗洛伊德算法总结:
- 时间复杂度是 O(|V|^3);
- 可以计算带负权值边的图中最短路径问题, 但是不能有包含负权值边组成的回路;
- 也适用于计算带权无向图的最短路径
拓扑排序
定义
有向无环图(DAG 图) : 若一个有向图中不存在环, 则称为有向无环图,简称 DAG 图。
拓扑排序: 我们对有向无环图的顶点进行排序, 使得若存在一条顶点 A 到顶点 B 的路径, 则在排序中顶点 B 一定出现在顶点 A 的后面, 我们把这种排序叫做拓扑排序。 对于同一个有向无环图, 拓扑排序序列可能有 1 个也可能有多个。
求拓扑序列
我们可以通过以下方法获取拓扑排序序列:
- 从有向无环图中选择一个没有前驱的顶点并输出;
- 删除该顶点, 并且删除以它为起点的有向边;
- 若图为空, 则算法结束; 若图非空且不存在没有前驱的顶点, 则说明有向图中存在环, 不存在拓扑序列; 否则, 跳转到第 1 步。
下图是求解拓扑序列的过程示例
关键路径
AOV 网和 AOE 网
AOV 网: 如果用 DAG 图表示一个工程, 其顶点表示活动, 用有向边<a,b>表示 a 活动必须早于 b 活动进行, 那么我们将它叫做 AOV 网(顶点表示活动的网络) 。
AOE 网: 如果用 DAG 图表示一个工程, 顶点表示事件, 有向边表示活动, 边上的权值表示活动的开销, 我们把这种图叫做 AOE 网(用边表示活动的网络) 。
AOE 网的几个性质:
- AOE 网只有一个开始顶点(入度为 0 的点, 如本图中的顶点 1),也只有一个结束顶点(出度为 0 的点, 如本图中的顶点 6)
- 只有在某顶点表示的事件发生后, 从该顶点出发的各有向边所代表的活动才能开始;
- 只有进入某顶点的所有有向边所代表的活动都完成时, 该顶点所代表的事件才能发生。 (在 AOE 网中有些活动是可以并行进行的, 如在上图中的活动 c 进行时,活动 a 也可以同时进行, 但 a 完成后, 必须等待 c 和 b 都完成, 事件 2 才可以发生, 进而 d、 e 才可以开始。 )
关键路径定义与意义
AOE 网中, 从开始顶点到结束顶点的路径中, 路径长度最大的那条路径我们称之为关键路径。 关键路径上的活动我们称之为关键活动。 关键路径的意义在于, 当我们按这个路径将关键活动全部完成时, 其他所有活动也可以并行完成。如果边上的权值代表完成这项活动的时间, 那么关键路径的总权值就代表整个工程完成的最短时间。
求解关键路径
- 求其拓扑序列;
- 对于 vev_{e}ve
a) 初始时, ve(i)=0v_{e}(i)=0ve(i)=0;
b) 按照拓扑序列求其余顶点的 vev_{e}ve;
c) 对于当前顶点(入度为0)viv_{i}vi, 计算其所有直接后继顶点vkv_{k}vk, 若ve(vi)+weight(vi,vk)>ve(vk)v_{e}(v_{i})+weight(v_{i}, v_{k})>v_{e}(v_{k})ve(vi)+weight(vi,vk)>ve(vk), 则记录其值, 将 max(ve(vi)+weight(vi,vk))max(v_{e}(v_{i})+weight(v_{i}, v_{k}))max(ve(vi)+weight(vi,vk))作为 ve(k)v_{e}(k)ve(k)的新值, 否则不更新; - 对于 vlv_{l}vl
a) 初始时, vl(i)=ve(n)v_{l}(i)=v_{e}(n)vl(i)=ve(n);
b) 按逆拓扑序列求其余顶点的 vlv_{l}vl;
c) 对于当前顶点viv_{i}vi(出度为0),计算其所有直接前驱顶点vkv_{k}vk, 若
vl(vi)−weight(vk,vi)<vl(vk)v_{l}(v_{i})-weight(v_{k}, v_{i})<v_{l}(v_{k})vl(vi)−weight(vk,vi)<vl(vk),则记录其值, 将 min(vl(vi)−weight(vk,vi))min(v_{l}(v_{i})-weight(v_{k}, v_{i}))min(vl(vi)−weight(vk,vi))作为 vl(k)v_{l}(k)vl(k)的新值,否则不更新; - 对于 eee
a) e(i)e(i)e(i)的值为该事件所在弧的起点的顶点的 ve()v_{e}()ve(); - 对于 lll
a) l(i)l(i)l(i)的值为该弧的终点的顶点的 vl()v_{l}()vl()-该弧的权值(活动持续时间); - 计算 l(i)−e(i)=0l(i)-e(i)=0l(i)−e(i)=0 的活动, 得出关键路径。
求解关键路径的示意图如下:
思考 1: 在上面的示意图中, 缩短活动 f 的时长到 9, 可以缩短整个项目的工期吗?
答: 不可以, f=9 时, 只剩一条关键路径, 那就是关键路径 1, 项目工期由它决定保持不变。
思考 2: 在上面的示意图中, 缩短 e 的时长到 5, 缩短 f 的时长到 9,可以缩短整个项目的工期吗?
答: 可以, 项目工期可以缩短 1。
思考 3: 在上面的示意图中, 缩短 e 的时长到 3, 缩短 f 的时长到 7,整个项目的工期可以缩短 3 吗?
答: 不能, 关键路径变为 1->3->2->4->6, 项目工期可以缩短 2。