一、采用深度优先搜索来遍历整个图得到DFS序列的步骤:
- 首先访问顶点v,并将其标记为已访问
- 检查v的邻接顶点,从中选一个尚未访问的顶点,从它出发继续进行深度优先搜索。将顶点v剩余的邻接顶点入栈。
- 重复以上操作,直到顶点v的多有邻接顶点都被访问。
- 如果图中还存在未访问的顶点,则选出一个未访问顶点,重复以上操作,直到所有的顶点都已被访问。
class StackUnderflow(ValueError): pass class SStack(): def __init__(self): self.elems = [] def is_empty(self): return self.elems == [] def top(self): #取得栈里最后压入的元素,但不删除 if self.elems == []: raise StackUnderflow('in SStack.top()') return self.elems[-1] def push(self, elem): self.elems.append(elem) def pop(self): if self.elems == []: raise StackUnderflow('in SStack.pop()') return self.elems.pop() class GraphError(ValueError): pass class Graph: def __init__(self, mat, unconn = 0): vnum = len(mat) for x in mat: if len(x) != vnum: #检查是否为方阵 raise ValueError('Argument for Graph') self.mat = [mat[i][:] for i in range(vnum)] self.unconn = unconn self.vnum = vnum def vertex_num(self): return self.vnum def invalid(self, v): return 0 > v or v >= self.vnum def add_edge(self, vi, vj, val=1): if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') self.mat[vi][vj] = val def get_edge(self, vi, vj): if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') return self.mat[vi][vj] #返回每个顶点的出边的终点位置,和这条出边上的权值 def out_edges(self, vi): if self.invalid(vi): raise GraphError(str(vi) + 'is not a valid vertex') return self._out_edges(self.mat[vi], self.unconn) @staticmethod def _out_edges(row, unconn): edges = [] for i in range(len(row)): if row[i] != unconn: #当前行中不等于0的位置 edges.append((i, row[i])) return edges class GraphAL(Graph): def __init__(self, mat1=[], unconn = 0): vnum = len(mat1) for x in mat1: if len(x) != vnum: raise ValueError('Argument for Graph') self.mat = [Graph._out_edges(mat1[i], unconn) for i in range(vnum)] self.unconn = unconn self.vnum = vnum def add_vertex(self): self.mat.append([]) self.vnum += 1 return self.vnum - 1 def add_edge(self, vi, vj, val=1): if self.vnum == 0: raise GraphError('can not add edge to empty graph') if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') row = self.mat[vi] #row是mat中的某一行。例如[(0,4),(2,6)] i = 0 while i < len(row): if row[i][0] == vj: #如果原来存在vi到vj的边,找出与终点vj相同的终点在第几个元组中 self.mat[vi][i] = (vj, val) return if row[i][0] > vj: #原来不存在vi到vj的边。因为边表中是按递增的顺序添加的, break #假设vj=2,但是当前已经遍历到了(3,1),说明没有终点为2的这条边 i += 1 self.mat[vi].insert(i,(vj, val)) def get_edge(self, vi, vj): if self.invalid(vi) or self.invalid(vj): raise GraphError(str(vi) + 'or' + str(vj) + 'is not a valid vertex') for i, val in self.mat[vi]: if i ==vj: return val return self.unconn def out_edges(self, vi): if self.invalid(vi): raise GraphError(str(vi) + 'is not a valid vertex') return self.mat[vi] def DFS(graph, v0): vnum = graph.vertex_num() visited = [0]*vnum visited[v0] = 1 #已访问过的顶点的位置赋1 DFS_seq = [v0] #将遍历的顶点加在序列中 st = SStack() st.push((0,graph.out_edges(v0))) #入栈(i,edges),edges是某个顶点的边表,i表示是边表中的第几个即edges[i] while not st.is_empty(): i, edges = st.pop() if i < len(edges): v, e = edges[i] #这个是当前顶点的边表中的第一个出边 st.push((i+1, edges)) #紧接着就将当前顶点边表中剩余的出边全部入栈,等沿着第一个出边向下遍历完后再回来访问这些出边 if not visited[v]: #如果顶点v未被访问过 DFS_seq.append(v) visited[v] = 1 st.push((0, graph.out_edges(v))) #将顶点v的边表入栈。如果顶点v的边表为空,则会压入(0,[]),在到达第一个if语句时 return DFS_seq #不符合条件判断了,再弹出栈中的一个元素 if __name__=="__main__": mat = [[0,0,3], [4,0,6], [0,8,9]] g1 = GraphAL(mat) print(DFS(g1,0))
二、用广度优先搜索来遍历整个图得到BFS序列的步骤:
- 先访问顶点v,并将其标记为已访问
- 依次访问v的所有邻接顶点v1,v2......vm,在依次访问与v1,v2......vm邻接的尚未访问的顶点。
- 如果图中还存在未访问的顶点,则选出一个未访问顶点,重复以上操作,直到所有的顶点都已被访问。
三、生成树
生成树: 如果连通图G有n个顶点,必然可以找到G中的一个包含n-1条边的边集合,这个集合里包含了从顶点v0到其他多有顶点的路径。由这n-1条边和n个顶点形成了图G的一个子图T,则称T是G的一棵生成树。
以下是一个生成树的例子及其代码实现。如果一个图有生成树,其生成树可能不唯一。
#span_forest中的元素为None时,说明当前顶点的路径未找到 #生成树的一个顶点可能有多个‘下一顶点’,但至多有一个‘前一顶点’。可以根据最后生成的span_forest,依次找前一顶点对应着画出生成树。 def DFS_span_forest(graph): vnum = graph.vertex_num() span_forest = [None]*vnum def dfs(graph, v): nonlocal span_forest #函数需要在后面修改这个值,所以声明为非局部变量 for u, w in graph.out_edges(v): if span_forest[u] is None: span_forest[u] = (v, w) #v是u的前一顶点,w是v到u边上的权值 dfs(graph, u) for v in range(vnum): if span_forest[v] is None: #当v=0时,由其生成的span_forest中的值都不为None,就直接返回span_forest;如果还有为None的位置,说明由当前顶点无法生成生成树。 # span_forest = [None]*vnum #如果由前面的顶点出发无法遍历全部的顶点,将其全置空 span_forest[v] = (v, 0) dfs(graph, v) return span_forest if __name__=="__main__": mat = [[0,0,1,2,0,0,0], [3,0,4,0,0,5,0], [0,6,0,0,7,0,0], [0,0,0,0,8,0,0], [0,0,0,0,0,0,9], [0,0,0,0,0,0,10], [0,0,0,0,0,0,0]] g1 = GraphAL(mat) print(DFS_span_forest(g1))
四、最小生成树(Minimun Spanning Tree)
基于带权连通无向图(网络)
生成树的权:网络G的一棵生成树中各条边的权值之和。
最小生成树:网络G中权值最小的生成树。 此外,任何一个网络都有最小生成树,但最小生成树可能不唯一。
1、Kruskal算法
- 首先,取出网络G中所有n个顶点,但不包含任何边,构成子图T。 T中的每个顶点都自成一个连通分量
- 将G中的所有边按照权值递增的顺序排序,从其中找出最短的且能使得连通分量减小的一条边。
- 重复步骤2,不断向T中加入新边,直到T中所有顶点有包含在一个连通分量里为止。
以下是一个例子,由网络G9生成的一棵最小生成树
(1)图1中,T中只包含所有顶点。
(2)图2中,选择图中的最短边(b,d),将其加入T中。
(3)图3中,再找一条最短边,当前有两条长为5的边都可以减少一个连通分量。任选其中的(a,b)加入T中。
(4)图4中,选择下一条最短边(c,f)加入到T中。
(5)图5中,再选择两条长为7的边和一条长为8的边。 此时最小生成树构造完成。
def Kruskal(graph): vnum = graph.vertex_num() reps = [i for i in range(vnum)] #列表的下标表示不同的顶点,列表中某些位置的元素如果数值相同, mst, edges = [], [] #表示这些位置的顶点已经被连接成了一个连通分量 for vi in range(vnum): #将所有的顶点所对应的出边的终点及其权值加入到edges中 for v, w in graph.out_edges(vi): #v是出边的终点,w是权值 edges.append((w, vi, v)) #vi是出边的始点 edges.sort() #将所有的边按照权值由小到大来排序。 for w, vi, vj, in edges: #按顺序依次取出边,先取最小的 if reps[vi] != reps[vj]: #如果reps列表中vi、vj位置的元素值不相等,说明vi、vj是两个不同的连通分量 mst.append(((vi, vj), w)) #如果不是同一个连通分量,则将这条边的始点和终点、权值存入mst中 if len(mst) == vnum-1: #如果mst中已经添加了n-1条边了,则终止程序 break rep, orep = reps[vi], reps[vj] #将两个不同的连通分量vi、vj合并为一个连通分量后,需要更新连通分量vj对应在 #reps表中的值,将其更新为vi对应在reps表中的值 for i in range(vnum): #遍历reps中所有位置 if reps[i] == orep: #如果有元素与vj对应在reps元素中值相等,则都更新为vi所对应的reps中的值 reps[i] = rep #因为vj连通分量里面可能对应着多个顶点,所以都需要将其进行更新 return mst if __name__=="__main__": inf = float('inf') mat = [[0 ,5 ,11 ,5 ,inf,inf,inf], [5 ,0 ,inf,3 ,9 ,inf,7 ], [11 ,inf,0 ,7 ,inf,6 ,inf], [5 ,3 ,7 ,0 ,inf,inf,20 ], [inf,9 ,inf,inf,0 ,inf,8 ], [inf,inf,6 ,inf,inf,0 ,8 ], [inf,7 ,inf,20 ,8 ,8 ,0 ]] g1 = GraphAL(mat) print(Kruskal(g1)) #[((1, 3), 3), ((0, 1), 5), ((2, 5), 6), ((1, 6), 7), ((2, 3), 7), ((4, 6), 8)]2、Prim算法
基本思想:从原图中任取一个顶点放入集合U中,找出以这个顶点为顶点的所有的边中权值最小的一条,将这条边的终点加入到集合U中,再从顶点集合U中的所有的边中选择则权值最小的一条。以此类推,直到顶点集合U中包含了原图中所有的顶点为止。
以下是一个例子。
(1)图1中,顶点集合U只包含顶点a。其一共有3条以a为顶点的边。
(2)图2中,从3条边中选择权值最小的一条(有两条权值为5的边,任选其中的(a,b))。
(3)图3中,选择当前权值最小的边(b,d)。
(4)图4中,选择当前权值最小的边(d,c)。
(5)图5中,继续加入(c,f),(b,g),(e,g)后,就得到了最终的生成树。
class Pri_Queue(object): def __init__(self, elems = []): self._elems = list(elems) self._elems.sort(reverse=True) def is_empty(self): return self._elems is [] def peek(self): if self.is_empty(): raise ListPriQueueValueError("in pop") return self._elems[-1] def dequeue(self): if self.is_empty(): raise ListPriQueueValueError("in pop") return self._elems.pop() def enqueue(self, e): i = len(self._elems) - 1 while i>=0: if self._elems[i] < e: i -= 1 else: break self._elems.insert(i+1, e) def Prim(graph): vnum = graph.vertex_num() mst = [None]*vnum #里面的每个位置将记录其前一顶点和权值 cands = Pri_Queue([(0,0,0)]) count = 0 #用来记录边数 while count < vnum and not cands.is_empty(): # print(cands._elems) w, u, v = cands.dequeue() if mst[v]: continue mst[v] = ((u, v), w) #u是始点,v是终点,w是权值 count += 1 for vi, w in graph.out_edges(v): if not mst[vi]: #如果终点vi对应的mst的值为None中,则终点vi不在顶点集合中,是一条侯选边 cands.enqueue((w, v, vi)) #w是权值,v是始点,vi是终点 return mst if __name__=="__main__": inf = float('inf') mat = [[0 ,5 ,11 ,5 ,inf,inf,inf], [5 ,0 ,inf,3 ,9 ,inf,7 ], [11 ,inf,0 ,7 ,inf,6 ,inf], [5 ,3 ,7 ,0 ,inf,inf,20 ], [inf,9 ,inf,inf,0 ,inf,8 ], [inf,inf,6 ,inf,inf,0 ,8 ], [inf,7 ,inf,20 ,8 ,8 ,0 ]] g1 = GraphAL(mat) print(Prim(g1)) #[((0, 0), 0), ((0, 1), 5), ((3, 2), 7), ((1, 3), 3), ((6, 4), 8), ((2, 5), 6), ((1, 6), 7)]五、最短路径
基于带权有向图和带权无向图(网络)
最短路径问题分为单源点最短路径(即从一个顶点出发到图中其余各顶点的最短路径问题),以及所有顶点之间的最短路径问题。
最短路径问题能够解决最短里程、最少运费、最低成本、最少时间等问题。
1、Dijkstra(狄克斯特拉)算法
该算法能解决给定顶点到图中所有其他顶点的最短路径。其有一个限制是:图中所有边的权值不能为负数。
算法的基本思想是:对于给定的顶点a,将其放入顶点集合U中,然后找到以顶点集合U中的点为顶点的所有边中权值最小的边,将这条边的终点也加入到顶点集合U中,然后更新从给定点a到达顶点集合U中能够直接关联到的顶点的已知最短路径的值;重复以上步骤,直到顶点集合U中包含所有n个顶点。
此外,如果这时还有未加入到U中的顶点,说明原图不连通。
例子如下,求解一个带权有向图中从顶点a出发到各个顶点的最短路径:
(1)图1中,只有a在集合U中,这时有两条边界边分别到顶点c和d
(2)图2中,选择距离a最近的d加入U并标记相应的边,标出新发现的到顶点e的边界边
(3)图3中,选择候选边界边中最短的边(a,c),并将c点加入集合U,此时又增加了3条新的边界边。此时,路径(a,c, e)比之前的路径(a,d,e)更近,所以更新到达e点的最短路径值。
(4)图4中,选择候选边界边中最短的边(c,e),并将e点加入集合U。
(5)图5中,此时,最短的候选边界边是(e,f),但是因为顶点b离a更近,所以先把顶点b加入集合U中。
(6)图6中,再按同样的方法加上最后两条边,即可得到所有的最短路径了。
说明程序中的一个小地方:图2 中已经得到了当前已有情况下a到e的路径是(a,d,e),长度为8,且已经被加入到优先队列中,但是在图3时,得到了一条新的路径(a,c,e),长度为7,也被加入到优先队列中,经过比较大小后,优先队列肯定先弹出最短的(a,c,e)。即使在之后的过程中(a,d,e)被弹出了,也会因为if paths[vmin]: continue这个判断语句,直接给pass掉。
def Dijkstra(graph, v0): vnum = graph.vertex_num() assert 0 <= v0 <vnum paths = [None]*vnum count = 0 cands = Pri_Queue([(0,v0,v0)]) while count < vnum and not cands.is_empty(): plength, u, vmin = cands.dequeue() #取当前边界边中最短的边的终点 if paths[vmin]: #如果到达这个终点的最短路径已经存在,则继续 continue paths[vmin] = (u, plength) #u是前一顶点,plength是总的长度,记录到达当前终点的前一顶点,以及a到此终点的总路径长度 count += 1 for v, w in graph.out_edges(vmin): if not paths[v]: #如果还没有到达当前顶点的最短路径 cands.enqueue((plength + w, vmin, v)) #plength+w是a到当前顶点的总的长度,vmin是前一顶点(也就是始点),vi是终点 return paths if __name__=="__main__": inf = float('inf') mat = [[0 ,inf,5 ,2 ,inf,inf,inf], [11 ,0 ,4 ,inf,inf,4 ,inf], [inf,3 ,0 ,inf,2 ,7 ,inf], [inf,inf,inf,0 ,6 ,inf,inf], [inf,inf,inf,inf,0 ,inf,2 ], [inf,inf,inf,inf,inf,0 ,3 ], [inf,inf,inf,inf,inf,inf,0 ]] g1 = GraphAL(mat) print(Dijkstra(g1,0)) #[(0, 0), (2, 8), (0, 5), (0, 2), (2, 7), (1, 12), (4, 9)]2、Floyd( 弗洛伊德 )算法
该算法是研究各对顶点之间最短路径的。该算法是基于图的邻接矩阵来实现实现的。
算法基本思想:如果想计算任意两个顶点vi到vj的最短路径,则先计算出从vi出发,途径的顶点可为所有顶点v0、v1...v(k-1),(0<=k<=n)中其中之一,再到vj的路径长度,再从中找出最短的一条即可。
def Floyd(graph): vnum = graph.vertex_num() a = [[graph.get_edge(i, j) for j in range(vnum)] for i in range(vnum)] #将邻接矩阵复制到a中 nvertex = [[-1 if a[i][j] == inf else j for j in range(vnum)] for i in range(vnum)] #如果a[i][j]为inf,则令nvertex[i][j]为-1(没有边),否则就令其为j,表示从vi到vj的路径上vi的后继顶点是vj。 for k in range(vnum): for i in range(vnum): for j in range(vnum): if a[i][j] > a[i][k] + a[k][j]: #如果途经k顶点的路径更短,则更新a和nvertex a[i][j] = a[i][k] + a[k][j] nvertex[i][j] = nvertex[i][k] return (a, nvertex) if __name__=="__main__": inf = float('inf') mat = [[0 ,inf,5 ,2 ,inf,inf,inf], [11 ,0 ,4 ,inf,inf,4 ,inf], [inf,3 ,0 ,inf,2 ,7 ,inf], [inf,inf,inf,0 ,6 ,inf,inf], [inf,inf,inf,inf,0 ,inf,2 ], [inf,inf,inf,inf,inf,0 ,3 ], [inf,inf,inf,inf,inf,inf,0 ]] g1 = GraphAL(mat) print(Floyd(g1)) #([[0, 8, 5, 2, 7, 12, 9], [11, 0, 4, 13, 6, 4, 7], [14, 3, 0, 16, 2, 7, 4], [inf, inf, inf, 0, 6, inf, 8], [inf, inf, inf, inf, 0, inf, 2], [inf, inf, inf, inf, inf, 0, 3], [inf, inf, inf, inf, inf, inf, 0]], [[0, 2, 2, 3, 2, 2, 2], [0, 1, 2, 0, 2, 5, 5], [1, 1, 2, 1, 4, 5, 4], [-1, -1, -1, 3, 4, -1, 4], [-1, -1, -1, -1, 4, -1, 6], [-1, -1, -1, -1, -1, 5, 6], [-1, -1, -1, -1, -1, -1, 6]])
六、AOV、拓扑排序/AOE网、关键路径
1、AOV(activity on vertex network)网:用图中的顶点表示不同的活动,用边表示各项活动之间的先后顺序。(不同都工作之间存在一些制约关系,只有当始点工作完成之后才能开始终点的工作)
在AOV网中,如果不存在回路,则所有活动可排列成一个线性序列,其中每个活动的所有前驱活动都排在该活动前面,这个序列被称为拓扑序列。而构造拓扑序列的的操作称为拓扑排序。 (如果存在回路,则某些活动的开始需要以其自身的完成作为先决条件,会出现死锁现象)
如果一个AOV网有拓扑排序,其拓扑排序可能不唯一。
上图中的两个拓扑序列是:C1,C2,C3,C4,C5,C6,C7,C8,C9,C10
: C2,C1,C4,C3,C6,C8,C5,C7,C10,C9
拓扑排序的步骤:
- 从AOV网中选出一个入度为0的顶点作为序列的下一顶点。
- 从AOV网中删除所选顶点及其所有的出边。
- 重复以上步骤,直到选出了图中的所有顶点,或没有入度非0的顶点时结束 。
如果剩下入度非0的顶点,说明原图中有回路。
*这个程序暂时没有调试成功。
def toposort(graph): vnum = graph.vertex_num() indegree, toposeq = [0]*vnum, [] zerov = -1 for vi in range(vnum): #遍历所有的顶点 for v, w in graph.out_edges(vi): #统计每个顶点的入度 indegree[v] += 1 for vi in range(vnum): #遍历所有顶点 if indegree[vi] == 0: #如果顶点的入度为0 indegree[vi] == zerov #第一个入度为0的位置将其置为-1,下一个入度为0的将其置为上一个入度为0位置的下标 zerov = vi #zerov是记录当前入度为0的顶点的下标 for n in range(vnum): if zerov == -1: return False vi = zerov #将当前入度为0的顶点的下标值从zerov中赋给vi zerov = indegree[zerov] #从indegree表中取出紧挨着当前位置的上一个入度为0但是还没有被添加到序列中的顶点的下标(也有可能是-1) toposeq.append(vi) #将当前入度为0的顶点下标加入到toposeq中 for v, w in graph.out_edges(vi): #取出当前入度为0的顶点对应的出边的的终点 indegree[v] -= 1 #将这些终点的入度分别减1 if indegree[v] == 0: #如果某个终点的入度在减1后,入度变为0了 indegree[v] == zerov #在下标(顶点)为V的地方记录上一个入度为0但是还没有被添加到序列中的顶点的下标 zerov = v #zerov记录当前入度为0顶点的下标,准备在下一次循环时将此下标加入到toposeq中 return toposeq if __name__=="__main__": inf = float('inf') # mat = [[0 ,inf,1 ,1 ,1 ,inf,inf,inf,inf,inf], # [inf,0 ,1 ,inf,inf,1 ,inf,inf,inf,inf], # [inf,inf,0 ,inf,inf,1 ,1 ,1 ,1 ,inf], # [inf,inf,inf,0 ,inf,1 ,1 ,1 ,inf,1 ], # [inf,inf,inf,inf,0 ,inf,1 ,inf,inf,inf], # [inf,inf,inf,inf,inf,0 ,inf,1 ,inf,inf], # [inf,inf,inf,inf,inf,inf,0 ,inf,1 ,1 ], # [inf,inf,inf,inf,inf,inf,inf,0 ,1 ,1 ], # [inf,inf,inf,inf,inf,inf,inf,inf,0 ,inf], # [inf,inf,inf,inf,inf,inf,inf,inf,inf,0 ]] mat = [[0,0,1,1,1,0,0,0,0,0], [0,0,1,0,0,1,0,0,0,0], [0,0,0,0,0,1,1,1,1,0], [0,0,0,0,0,1,1,1,0,1], [0,0,0,0,0,0,1,0,0,0], [0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,0,1,1], [0,0,0,0,0,0,0,0,1,1], [0,0,0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0,0,0]]
2、AOE(activity on edge netmork)网是另一类常用的带权有向无环图。其中的顶点表示时间,有向边表示活动,边上的权值表示活动的持续时间。
图中的ai:n表示活动名为ai,权值为n。整个工程开始,活动a0、a1、a2就可以同时开始了,而活动a3、a4需要等到事件v1发生后才能开始。时间v8表示整个工程的结束。
关键路径:完成整个工程的最短时间,是从开始顶点到结束顶点的最长路径上各边的权值之和。这种最长路径被称为关键路径。
关键路径算法基本思想:
- 首先确定vj的最早可能发生时间ee[j],其可以递推得到:
这里解释一下为什么最早发生时间是把权值最大的路径都加起来:看上图中的事件v2,事件v2完成,需要活动a1、a4、 全部完成,所以是这2个中耗时最长的活动制约着事件v2的开始时间,所以将权值最大的路径加起来才是事件的 最早开始时 间。
- 确定事件vi的最迟允许发生时间le[j],其可以反向递推得到:
最迟允许发生时间就是后一结点的最早可能开始时间减去其所有入边中耗时最长的活动的耗时。事件v2的最迟允许发生时间是由 活动a1制约着,预留出恰好能让活动a1完成的时间,在这个时间内活动a4肯定能够完成。
- 如果某个活动的最早可能开始时间ee[k]‘等于’最迟允许发生时间le[k],则称这个活动为关键活动。完全由关键活动从开始到结束构成的的路径就是关键路径。
以下是一个例子,方括号中第一个数字是最早可能开始时间,第二个数字是最迟允许发生时间。
def critical_paths(graph): def events_earliest_time(vnum, graph, toposeq): ee = [0]*vnum for i in toposeq: for j, w in graph.out_edges(i): if ee[i] + w > ee[j]: ee[j] = ee[i] + w return ee def event_latest_time(vnum, graph, toposeq, eelast): le = [eelast]*vnum for k in range(vnum-2, -1, -1): i = toposeq[k] for j, w in graph.out_edges(i): if le[i] - w < le[i]: le[i] = le[j] - w return le def crt_paths(vnum, graph, ee, le): crt_actions = [] for i in range(vnum): for j, w in graph.out_edges(i): if ee[i] == le[j] - w: crt_actions.append((i, j, ee[i])) return crt_actions toposeq = toposort(graph) if not toposeq: return False vnum = graph.vertex_num() ee = events_earliest_time(vnum, graph, toposeq) le = event_latest_time(vnum, graph, toposeq, ee[vnum-1]) return crt_paths(vnum, graph, ee, le)