拓扑排序精讲:
拓扑排序背景
文件存在依赖关系,需要给出一条线性的依赖顺序来处理这些文件,而许多文件的依赖关系又可以画成一张有向图。
概括来说,就是给出一个有向图,把这个有向图转成线性的排序就叫拓扑排序。
而拓扑排序也可以用来检测有向图中是否存在环,即存在循环依赖的情况。
思路
拓扑排序是一种解决问题的大致思路,具体的算法,可能是广搜或者深搜。一般来说,BFS是要掌握的思路,最为清晰易懂。
拓扑排序的步骤:
-
找到入度为0的节点
-
从图中删除该节点
循环上面两步,直到所有节点都被删除。最后所得到的结果,就是输出的拓扑排序顺序。(集里顺序可能不唯一) -
首先统计节点的入度,并使用邻接表记录节点。这样便于在后面删除节点时,将其所连的节点的入度 - 1
-
使用队列保存入度为0的节点
-
然后BFS,出队节点,并将其所连节点的入度 - 1,如果减完之后入度为0,再将其入队
-
result保存BFS过程中出队的节点,最后如果图中还有节点,即len(result) != 图节点个数,那么就说明存在循环。输出-1;否则输出result
代码
from collections import defaultdict
from collections import deque
def main():
n, m = map(int, input().split())
graph = defaultdict(list) # 邻接表
inDegree = [0] * n
for _ in range(m):
s, t = map(int, input().split())
graph[s].append(t)
inDegree[t] += 1
# 用队列保存入度为0的节点
que = deque()
for i in range(n):
if inDegree[i] == 0:
#print(i)
que.append(i)
result = [] # 输出文件顺序
while len(que):
cur = que.popleft()
result.append(cur)
# 在图中删除该节点,也就是将节点指向的所有节点的入度-1
for i in graph[cur]:
inDegree[i] -= 1
if inDegree[i] == 0: # 减完之后入度为0的节点入队
que.append(i)
# 输出
if len(result) == n:
#print(1)
print(" ".join(map(str, result)))
else:
print(-1)
if __name__ == '__main__':
main()
dijstra(朴素版)精讲:
思路
dijstra算法针对带权有向图求某个节点到其它节点的最短路径长度,且要求权值不能为负值,不然的话会错过真正的最短距离。
dijstra的思路与prim算法非常相似,都是三部曲
- 找到距离源点最近的点
- 标记该点
- 更新miniDist数组。
其中更新miniDist数组是,更新没有标记过且当前距离源点距离 > cur距源点距离 + cur和 i 之间距离。这也是dijstra和prim代码上的区别,而且也因为更新miniDist数组需要用到cur 和 i 之间的距离,由于节点距离源点的距离和cur和i之间的距离初始化均为float(‘inf’),所以更新的判断条件为:if not visited[i] and graph[cur][i] != float('inf') and miniDist[cur] + graph[cur][i] < miniDist[i]
代码
def print_dist(miniDist):
print(" ".join(map(str, miniDist)))
def main():
n, m = map(int, input().split())
graph = [[float('inf')] * (n + 1) for _ in range(n + 1)] # 节点编号从1开始
for _ in range(m):
s, e, v = map(int, input().split())
graph[s][e] = v
miniDist = [float('inf')] * (n + 1) # 下标与节点编号相对应
visited = [False] * (n + 1) # 记录节点是否被访问过
miniDist[1] = 0 # 源点到源点的距离为0,(这里要记得设置)
for i in range(1, n + 1): # 得到源点到图上所有点的最短距离
# 找到距离源点最短的点
cur, mindist = -1, float('inf')
for i in range(1, n + 1):
if not visited[i] and miniDist[i] < mindist:
cur, mindist = i, miniDist[i]
# 设置该点访问过
visited[cur] = True
# 更新miniDist数组
for i in range(1, n + 1):
# 更新没有被访问过的节点,且cur距离+graph[cur][i] < miniDist[i]
if not visited[i] and graph[cur][i] != float('inf') and miniDist[cur] + graph[cur][i] < miniDist[i]:
miniDist[i] = miniDist[cur] + graph[cur][i]
#print_dist(miniDist)
# 输出小明从开始到结束的最小距离
if miniDist[n] == float('inf'):
print(-1) # 不能到达
else:
print(miniDist[n])
if __name__ == '__main__':
main()
时间复杂度O(n^2)
如果要输出具体的路径
和prim的方法相同,使用parent数组记录节点的上一个节点
def print_parent(parent, n):
i = n
while parent[i] != i:
print(str(parent[i]) + "->" + str(i))
i = parent[i]
miniDist = [float('inf')] * (n + 1) # 下标与节点编号相对应
visited = [False] * (n + 1) # 记录节点是否被访问过
parent = [x for x in range(n + 1)] # 记录路径
miniDist[1] = 0 # 源点到源点的距离为0
# 更新miniDist数组
for i in range(1, n + 1):
# 更新没有被访问过的节点,且cur距离+graph[cur][i] < miniDist[i]
if not visited[i] and graph[cur][i] != float('inf') and miniDist[cur] + graph[cur][i] < miniDist[i]:
miniDist[i] = miniDist[cur] + graph[cur][i]
parent[i] = cur
#print_dist(miniDist)
# 输出小明从开始到结束的最小距离
if miniDist[n] == float('inf'):
print(-1) # 不能到达
else:
print(miniDist[n])
#print_parent(parent, n)
prim算法求的是节点距离最小生成树的距离(无向图),因此用graph[cur][i] < miniDist[i]即可,而dijstra判断的是节点距离源点的距离(有向图),因此要用cur.val + graph[cur][i] +< miniDist[i]
学习收获:
本次都是有向图
拓扑排序:找到入度为0的节点,在图中删除节点(代码中是将其连接的节点的入度 - 1),循环上面的操作,直到找不到入度为0的节点。如果最后图中还有节点,那么说明存在循环。
dijstra算法:找到距离源点最近的点,打标记,更新miniDist数组(即更新未打标记且距离源点距离miniDist[i] > miniDist[cur] + graph[cur][i],且要求graph[cur][i] != float(‘inf’))(因为miniDist初始化为float(‘inf’),当miniDist[i] == graph[cur][i] = float(‘inf’)时,上面不等式也是成立的)