数据结构之拓扑排序和关键路径


  数据结构是程序设计的重要基础,它所讨论的内容和技术对从事软件项目的开发有重要作用。学习数据结构要达到的目标是学会从问题出发,分析和研究计算机加工的数据的特性,以便为应用所涉及的数据选择适当的逻辑结构、存储结构及其相应的操作方法,为提高利用计算机解决问题的效率服务。
  数据结构是指数据元素的集合及元素间的相互关系和构造方法。元素之间的相互关系是数据的 逻辑结构,数据元素及元素之间关系的存储称为 存储结构(或物理结构)。数据结构按照逻辑关系的不同分为 线性结构非线性结构两大类,其中,非线性结构又可分为树结构和图结构。

1、AOV 网

  在工程领域,一个大的工程项目通常被划分为许多较小的子工程(称为活动)。显然,当这些子工程都完成时,整个工程也就完成了。在有向图中,若以顶点表示活动,用有向边表示活动之间的优先关系,则称这样的有向图为以顶点表示活动的网(Activity On Vertex network,AOV 网)。在 AOV 网中,若从顶点vi到顶点vj有一条有向路径,则顶点vi是vj的前驱,顶点vj是vi的后继。若<vi,vj>是网中的一条弧,则顶点vI是vj的直接前驱,顶点vj是vi的直接后继。AOV 网中的弧表示了活动之间的优先关系,也可以说是一种活动进行时的制约关系。
  在 AOV 网中不应出现有向环,若存在,则意味着某项活动必须以自身任务的完成为先决条件,显然这是荒谬的。因此,若要检测一个工程是否可行,首先应检查对应的AOV网是否存在回路。不存在回路的有向图称为有向无环图,或DAG(Directed Acycline Graph)图。检测的方法是对有向图构造其顶点的拓扑有序序列。若图中所有的顶点都在它的拓扑有序序列中,则该 AOV 网中必定不存在环。

2、拓扑排序及其算法

  拓扑排序是将 AOV网中的所有顶点排成一个线性序列的过程,并且该序列满足:若在AOV网中从顶点vi到vj有一条路径,则在该线性序列中,顶点vi必然在顶点vj之前。
  一般情况下,假设 AOV 图代表一个工程计划,则 AOV 网的一个拓扑排序就是一个工套顺利完成的可行方案。对 AOV 网进行拓扑排序的方法如下。
  (1)在AOV网中选择一个入度为0(没有前驱)的顶点且输出它。
  (2)从网中删除该顶点及与该顶点有关的所有弧。
  (3)重复上述两步,直到网中不存在入度为0的顶点为止。
  执行的结果会有两种情况:一种是所有顶点已输出,此时整个拓扑排序完成,说明网中不存在回路;另一种是尚有未输出的顶点,剩余的顶点均有前驱顶点,表明网中存在回路,拓排序无法进行下去。对于下图(a)所示的有向无环图进行拓扑排序,得到的拓扑序列为6,1,4,3,2,5。
在这里插入图片描述

  当有向图中无环时,也可以利用深度优先遍历进行逆拓扑排序。由于图中无环,从图中某点出发进行深度优先遍历时,最先退出 Dfs函数的顶点即是出度为0的顶点,它是拓扑有序序列中最后的一个顶点。由此,按退出 Ds函数的先后顺序记录下来的顶点序列即为逆向的拓扑有序序列。拓扑排序算法的时间复杂度为 O(n+e)。

3、AOE 网

  若在带权有向图G中以顶点表示事件,以有向边表示活动,以边上的权值表示该活动持续的时间,则这种带权有向图称为用边表示活动的网(Activity On Edge network,AOE 网)。通常在 AOE 网中列出了完成预定工程计划所需进行的活动、每项活动的计划完成时间、活动开始或结束的事件以及这些事件和活动间的关系,从而可以分析该项工程是否实际可行并估计工程完成的最短时间,以及影响工程进度的关键活动;进一步可以进行人力、物力的调度和分配,以达到缩短工期的目的。
  在用 AOE 网表示一项工程计划时,顶点所表示的事件实际上就是某些活动已经完成、某些活动可以动工的标志。具体来说,顶点所表示的事件是指该顶点所有进入边所表示的活动均已完成、从它出发的边所表示的活动均可以开始的一种事件。
  一般情况下,每项工程都有一个开始事件和一个结束事件,所以在 AOE 网中至少有一个入度为0的开始顶点,称为源点。另外,应有一个出度为0的结束顶点,称为汇点。AOE网中不应存在有向回路,否则整个工程无法完成。
  与 AOV 网不同,AOE 网所关心的问题如下:
  (1)完成该工程至少需要多少时间?
  (2)哪些活动是影响整个工程进度的关键?
  由于 AOE 网中的某些活动能够并行地进行,因此完成整个工程所需的时间是从开始顶点到结束顶点的最长路径的长度。这里的路径长度是指该路径上的权值之和。

4、关键路径和关键活动

  在从源点到汇点的路径中,长度最长的路径称为关键路径。关键路径上的所有活动均是关键活动。如果任何一项关键活动没有按期完成,就会影响整个工程的进度,而缩短关键活动的工期通常可以缩短整个工程的工期。假设在n个顶点的 AOE 网中,顶点v0表示源点、顶点 vn-1表示汇点,则引入顶点事件的最早、最晚发生时间,活动的最早、最晚开始时间等概念。
  (1)顶点事件的最早发生时间 ve(j)。ve(j)是指从源点v0到vj的最长路径长度(时间)。这个时间决定了所有从vj发出的弧所表示的活动能够开工的最早时间。
  ve(j)计算方法为
= { v e ( j ) = m a x { v e ( i ) + d u t ( < i , j > ) } < i , j > ∈ T , 1 ≤ j ≤ n − 1 v e ( 0 ) = 0 =\huge\{^{ve(0)=0} _{ve(j)= max\{ve(i)+dut(<i,j>)\} \quad <i,j>∈T,1≤ j≤ n-1} ={ve(j)=max{ve(i)+dut(<i,j>)}<i,j>∈T,1jn1ve(0)=0
其中,T是所有到达顶点 j 的弧的集合,dut(<i,j>)是弧<i,j>上的权值,n是网中的顶点数,如下图(a)所示。
  显然,上式是一个从源点开始的递推公式。显然,必须在vj的所有前驱顶点事件的最早发生时间全部得出后才能计算 ve(j)。这样必须对AOE网进行拓扑排序,然后按拓扑有序序列逐个求出各顶点事件的最早发生时间。
  (2)顶点事件的最晚发生时间 vl(i)。vl(i)是指在不推迟整个工期的前提下,事件vi的最晚发生时间。对于一个工程来说,计划用几天时间完成是可以从AOE网求得的,其数值就是汇点 vn-1的最早发生时间 ve(n-1),而这个时间也就是 vl(n-1)。其他顶点事件的vl应从汇点开始,逐步向源点方向递推才能求得,所以 vl(i)的计算公式为
= { v l ( i ) = m i n { v l ( j ) − d u t ( < i , j > ) } < i , j > ∈ S , 1 ≤ j ≤ n − 2 v l ( n − 1 ) = v e ( n − 1 ) =\huge\{^{vl(n-1)=ve(n-1)} _{vl(i)= min\{vl(j)-dut(<i,j>)\} \quad <i,j>∈S,1≤ j≤ n-2} ={vl(i)=min{vl(j)dut(<i,j>)}<i,j>∈S,1jn2vl(n1)=ve(n1)
其中,S是所有从顶点 i 发出的弧的集合,如下图(b)所示。
在这里插入图片描述

  显然,必须在顶点vi的所有后继顶点事件的最晚发生时间全部得出后才能计算 vl(i)。这样必须对 AOE网逆拓扑排序,由逆拓扑序列递推计算各顶点的 vl 值。
  (3)活动ak的最早开始时间 e(k)。e(k)是指弧<i,j>所表示的活动 ak 最早可开工时间。
         e(k)= ve(i)
  这说明活动 ak 的最早开始时间等于事件 vi 的最早发生时间。
  (4)活动 ak 的最晚开始时间l(k)。l(k)是指在不推迟整个工期的前提下,该活动的最晚开始时间。若活动 ak 由弧<i,j>表示,则
         l(k)= vl(j)-dut(<i,j>)
  对于活动 ak 来说,若 e(k)=l(k),则表示活动 ak 是关键活动,它说明该活动最早可开工时间与整个工程计划允许该活动最晚的开工时间一致,施工期一点也不能拖延。若活动 ak 不能按期完成,则工程将延期;若活动 ak 提前完成,则可能使整个工程提前完工。
  由关键活动组成的路径是关键路径。依照上述计算关键活动的方法,即可形成AOE网的关键路径。

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我们先来了解一下什么是拓扑排序关键路径拓扑排序是对有向无环图(DAG)进行排序的一种方法。它可以将一个DAG的顶点排成一条线性序列,使得对于任何一条有向边 (u, v),顶点 u 在序列中都排在顶点 v 的前面。 关键路径是指在一个有向无环图中,从起点到终点的所有路径中,耗时最长的那条路径。在实际应用中,关键路径可以用来确定项目的最短工期,以及哪些任务是关键任务,不能延误。 下面是用C语言实现拓扑排序关键路径的代码,注释中有详细的解释。 ```c #include <stdio.h> #include <stdlib.h> #define MAX_VERTEX_NUM 100 // 图中最大顶点数 #define MAX_EDGE_NUM 100 // 图中最大边数 // 边的结构体,包含起点和终点 typedef struct { int from; // 起点 int to; // 终点 } Edge; // 顶点的结构体,包含入度和出度 typedef struct { int in; // 入度 int out; // 出度 } Vertex; // 图的结构体,包含顶点数组、边数组、顶点数和边数 typedef struct { Vertex vertices[MAX_VERTEX_NUM]; // 顶点数组 Edge edges[MAX_EDGE_NUM]; // 边数组 int vertex_num; // 顶点数 int edge_num; // 边数 } Graph; // 初始化图 void init_graph(Graph *g) { int i; g->vertex_num = 0; g->edge_num = 0; for (i = 0; i < MAX_VERTEX_NUM; i++) { g->vertices[i].in = 0; g->vertices[i].out = 0; } } // 添加边 void add_edge(Graph *g, int from, int to) { g->edges[g->edge_num].from = from; g->edges[g->edge_num].to = to; g->edge_num++; g->vertices[from].out++; // 起点出度加1 g->vertices[to].in++; // 终点入度加1 } // 拓扑排序 void topological_sort(Graph *g) { int i, j, k, n; int queue[MAX_VERTEX_NUM]; // 存储入度为0的顶点 int head = 0, tail = 0; // 队列头和尾 int count = 0; // 已排序的顶点数 Edge *e; // 将入度为0的顶点加入队列 for (i = 0; i < g->vertex_num; i++) { if (g->vertices[i].in == 0) { queue[tail++] = i; } } // 循环直到队列为空 while (head != tail) { n = tail - head; // 当前队列中的顶点数 for (i = 0; i < n; i++) { j = queue[head++]; // 取出队列头 printf("%d ", j); // 输出已排序的顶点 count++; // 已排序的顶点数加1 for (k = 0; k < g->edge_num; k++) { e = &g->edges[k]; if (e->from == j) { // 找到以j为起点的边 g->vertices[e->to].in--; // 对应终点的入度减1 if (g->vertices[e->to].in == 0) { // 如果终点入度为0,则加入队列 queue[tail++] = e->to; } } } } } if (count < g->vertex_num) { // 如果已排序的顶点数小于总顶点数,则存在环路 printf("The graph has a cycle\n"); } } // 计算关键路径 void critical_path(Graph *g) { int i, j, k, m = 0, n = 0; int earliest[MAX_VERTEX_NUM] = {0}; // 存储最早开始时间 int latest[MAX_VERTEX_NUM] = {0}; // 存储最晚开始时间 Edge *e; // 计算最早开始时间 for (i = 0; i < g->vertex_num; i++) { for (j = 0; j < g->edge_num; j++) { e = &g->edges[j]; if (e->from == i) { // 找到以i为起点的边 if (earliest[e->to] < earliest[i] + 1) { // 更新终点的最早开始时间 earliest[e->to] = earliest[i] + 1; } } } } // 计算最晚开始时间 for (i = g->vertex_num - 1; i >= 0; i--) { latest[i] = earliest[g->vertex_num - 1]; // 先初始化为总工期 for (j = 0; j < g->edge_num; j++) { e = &g->edges[j]; if (e->from == i) { // 找到以i为起点的边 if (latest[i] > latest[e->to] - 1) { // 更新起点的最晚开始时间 latest[i] = latest[e->to] - 1; } } } } printf("The critical path is: "); for (i = 0; i < g->edge_num; i++) { e = &g->edges[i]; if (earliest[e->to] - earliest[e->from] == latest[e->to] - latest[e->from]) { // 如果边的最早开始时间和最晚开始时间相等,则为关键边 printf("(%d,%d) ", e->from, e->to); if (earliest[e->to] > m) { // 找到最大的最早开始时间 m = earliest[e->to]; } if (latest[e->from] < n) { // 找到最小的最晚开始时间 n = latest[e->from]; } } } printf("\nThe minimum time to finish the project is %d\n", m - n); } int main() { Graph g; init_graph(&g); g.vertex_num = 7; // 设置顶点数 add_edge(&g, 0, 1); add_edge(&g, 0, 2); add_edge(&g, 1, 3); add_edge(&g, 1, 4); add_edge(&g, 2, 3); add_edge(&g, 2, 5); add_edge(&g, 3, 6); add_edge(&g, 4, 6); add_edge(&g, 5, 6); topological_sort(&g); critical_path(&g); return 0; } ``` 上述代码中,我们定义了一个Graph的结构体来表示图,包含顶点数组、边数组、顶点数和边数。同时定义了一个Vertex的结构体来表示顶点,包含入度和出度。定义了一个Edge的结构体来表示边,包含起点和终点。然后分别实现了初始化图、添加边、拓扑排序和计算关键路径的函数。 在main函数中,我们先初始化图,然后添加边,设置顶点数为7。然后调用拓扑排序和计算关键路径的函数来输出结果。 这段代码可能对于数据结构初学者来说有些难度,但是只要认真看注释,理解了拓扑排序关键路径的原理,就能够理解代码的实现过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值