Bootstrap

代码随想录算法训练营第五十八天 | 拓扑排序 dijstra

拓扑排序精讲:

文章链接
题目链接:117.软件构建

拓扑排序背景

文件存在依赖关系,需要给出一条线性的依赖顺序来处理这些文件,而许多文件的依赖关系又可以画成一张有向图。
概括来说,就是给出一个有向图,把这个有向图转成线性的排序就叫拓扑排序。
而拓扑排序也可以用来检测有向图中是否存在环,即存在循环依赖的情况。

思路

拓扑排序是一种解决问题的大致思路,具体的算法,可能是广搜或者深搜。一般来说,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(朴素版)精讲:

文章链接
题目链接:47.参加科学大会

思路

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’)时,上面不等式也是成立的)


;