我原来是学工业工程出身,第一份工作出来做的是生产线优化。在一条流水线上,总是会出现一个工作之前要有很多个前置工作,而前置工作往往是可以并行的。
就比如你要组装一辆汽车,那么组装这个操作之前就有很多个并行的前置工作,比如加工轮胎,加工地盘,加工发动机等等。这些单独的前置工作又会有各自的前置,这就形成了一个前后制约的流水线。
在流水线中,往往会有一条加工线,他花费的时间最长,制约着整个流水线最终的效率。那这样的一条加工线,就是关键线路,我当时在生产线上的工作就是找到这个关键线路,然后优化其中制约效率的结点,从而让整体的生产效率提升。
当年因为负责的生产型并非十分的复杂,所以这种工作用图表也就可以勉强的解决了。那如果是加工一辆汽车,有成千上万个零件和对应的工序,那么想在这中间找到那条制约整个加工效率的关键路径就并不是非常的容易了。这时候就需要借助计算机的帮助。
用计算机的思路对这个问题进行抽象,就可以把加工的流水线看做是一个有向无环的网,其中每一条弧代表一个加工作业,而每一个弧上的权重代表着这个作业花费的时间,而弧和结点的相互顺序,代表了作业的前置依赖。我想要找到的也就是这个网中,从起始点到终点的关键路径。
图中A,B是两个起始节点,他们的入度为0,而I为最终节点,他的出度为0。那我们的目的,就是要找到从A,B到I的路径,这条路径上的加工作业时间决定了整体加工作业的用时。
关键路径有什么特征?
我们知道一个工作有最早的开始时间也有最晚的开始时间。就比如你老板给了你个工作,要求是今天下班前给他。这个工作之前你需要小张把材料给你,但他说要下午两点才能给你。你评估了一下这个工作只要2个小时就可以完成了。而下午两点到下班中间有四个小时。
那么你这个工作最早的开始时间就是下午2点,再早了你都没有材料,那最晚的开始时间是下午四点,因为再晚你6点下班前给不出东西来了。所以你这个工作就有两个时间最早开始和最晚开始时间,但是他们并不一样(一个2点,一个4点)。
那你出去了解了一下,老板之所以是下班才要这个东西,是因为他同时还让小赵做了另一个材料,你们俩的工作合在一起才有用。而小赵这个东西很麻烦要4个小时,他也是最早下午2点才能从小王那边拿到材料。所以小赵的工作最早开始时间是2点,最晚开始时间也是2点。
那想让老板更早拿到成品,提升你的效率还是提升小赵的呢?当然是小赵的,因为你做的再快也不会减少这个流程的时间了。这时候小赵的工作就是关键路径。他有什么特征呢?
关键路径的特征,最早开始时间和最晚开始时间是一样的,他没有调整的空间。
怎么求出最早开始时间和最晚开始时间?
想要求出边的最早开始时间和最晚开始时间,我们需要借助结点的最早开始时间和最晚开始时间。假设对于一条弧,那么他最早的开始时间,就是结点D的最早开始时间。而最晚开始时间,就是G的最晚开始时间减去弧的权重。所以只要知道了结点最早,最晚开始时间,就可以算出来弧的时间。
那么如何求出结点的最早开始时间和最晚开始时间呢?
我们可以利用拓扑排序的方式,来计算出结点前置路径最大的那一个,就是这个节点的最早开始时间了。这个的含义是这个结点前面的工作中最慢的一个要用这么久,所以这个结点最早开始时间。
而获得最晚开始时间,则把拓扑排序的过程反过来。先得到最后一个结点的完成时间,倒推回去,取后置结点的最晚时间减去权重的最小值。这个含义是deadline已经确定了,工作时间也确定了,你至少要比这个时间早开始才能做得完。
举个例子,就拿G结点来说。他的最早开始时间是比较一下到达他的前面几条路径,分别是=6,=8,=8,=6。因为其中最长耗时的路径为8,所以G点不能比8点开始更早了。
同样的求G的最晚开始时间,假设已经知道I的最晚开始时间为13。因为=3,所以G的最晚开始时间就是13-3=10。
这样我们就有了结点的最早,最晚开始时间,也就可以算出边的最早最晚开始时间,进而得到关键路径了。
代码实现
因为需要知道结点的前后边,所以图的存储结构采用十字链表(图的存储方式(二)),关键路径的实现如下。
# 关键路径算法def critical_path(orthogonal_graph): # 初始化最早结点开始时间 etv = [0 for i in range(len(orthogonal_graph.vertex_list))] # 初始化拓扑序列栈 stack = queue.LifoQueue() # 进行拓扑排序,将拓扑排序节点压如栈中,更新最早结点开始时间 # 建立入度为0的节点队列 q_ready = queue.Queue() # 拓扑序列 topo_list = [] # 计算图中每一个节点的入度 for v_index in range(len(orthogonal_graph.vertex_list)): v = orthogonal_graph.vertex_list[v_index] # 初始入度为0 v.in_degree = 0 if v.first_in is None: # 入度为0放入到准备队列 q_ready.put(v_index) else: e = v.first_in # 遍历边集列表,计算入度 while e is not None: v.in_degree += 1 e = e.head_link # 如果入度为空队列里面有节点,就处理,没有待处理的节点,就结束输出拓扑序列 while not q_ready.empty(): v_index = q_ready.get() # 该处与拓扑序列操作不同,将结点压如栈中 stack.put(v_index) topo_list.append(v_index) # 更新相邻指向节点的入度 e = orthogonal_graph.vertex_list[v_index].first_out while e is not None: v_next = orthogonal_graph.vertex_list[e.head_vex] v_next.in_degree -= 1 # 如果更新之后的节点入度为零,加入到待处理列表中 if v_next.in_degree == 0: q_ready.put(e.head_vex) # 更新结点最小开始时间 etv[e.head_vex] = max(etv[e.head_vex],etv[e.tail_vex]+e.weight) # 指向下一条边 e = e.tail_link # 初始化最晚结点开始时间,所有的节点时间设置为最后一个结点的开始时间。 ltv = [etv[-1] for i in range(len(orthogonal_graph.vertex_list))] # 反向操作拓扑序列,计算每一个结点的最晚开始时间 while not stack.empty(): # 从栈中取出拓扑序列最后面的结点 v_index = stack.get() v = orthogonal_graph.vertex_list[v_index] # 遍历指向该节点的弧,更新结点的最晚开始时间。 e = v.first_in while e is not None: # 更新最晚开始时间 ltv[e.tail_vex] = min(ltv[e.tail_vex],ltv[e.head_vex]-e.weight) # 更新下一条指向该结点的弧 e = e.head_link print('etv',etv) print('ltv',ltv) # 求关键路径,弧的最早开始时间和最晚开始时间一样的时候,则为关键路径。 # 求弧的最早开始时间和最晚开始时间。 for v_index in range(len(orthogonal_graph.vertex_list)): # 结点 v = orthogonal_graph.vertex_list[v_index] # 结点的出弧 e = v.first_out # 遍历弧 while e is not None: # 弧的最早开始时间 ete = etv[e.tail_vex] # 弧的最晚开始时间 lte = ltv[e.head_vex] - e.weight if ete == lte: print(' ' % (orthogonal_graph.vertex_list[e.tail_vex].data,orthogonal_graph.vertex_list[e.head_vex].data),end='') # 指向下一个弧 e = e.tail_link
实验一下上面那张图,看看关键路径是什么
vertex_list = ['A','B','C','D','E','F','G','H','I']graph_matrix = [[0,INFINITY,1,2,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY], [INFINITY,0,INFINITY,2,3,INFINITY,INFINITY,INFINITY,INFINITY], [INFINITY,INFINITY,0,INFINITY,INFINITY,4,5,INFINITY,INFINITY], [INFINITY,INFINITY,INFINITY,0,INFINITY,INFINITY,6,INFINITY,INFINITY], [INFINITY,INFINITY,INFINITY,INFINITY,0,INFINITY,3,7,INFINITY], [INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,0,INFINITY,INFINITY,5], [INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,0,3,4], [INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,0,2], [INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,INFINITY,0]]orth_graph = OrthogonalGraph()orth_graph.build(vertex_list,graph_matrix)critical_path(orth_graph)
结果显示的关键路径如下:
etv [0, 0, 1, 2, 3, 5, 8, 11, 13]ltv [0, 0, 3, 2, 4, 8, 8, 11, 13]
END
作者:锅哥不姓郭
图片:网络(侵删)