图论 18. dijkstra算法(朴素版)(以及dijkstra与prim的区别)

图论 18. dijkstra算法(朴素版)(以及dijkstra与prim的区别)

47. 参加科学大会(第六期模拟笔试)

代码随想录

卡码网无难度标识

  • 思路:(摘录修改自代码随想录)

    • 题目解读:

      本题就是求最短路

      最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。

      接下来,我们来详细讲解最短路算法中的 dijkstra 算法。

    • dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。

      需要注意两点:

      • dijkstra 算法可以同时求 起点到所有节点的最短路径
      • 权值不能为负数

      dijkstra 算法 和 prim算法思路非常接近

      dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。

      在dijkstra算法中,同样有一个数组很重要,起名为:minDist。

      minDist数组 用来记录 每一个节点距离源点的最小距离

    • dijkstra初始化+三部曲:(具体模拟的流程可以见代码随想录模拟过程)

      以本题示例中的图为例:

      图论 18. dijkstra算法(朴素版)(以及dijkstra与prim的区别)_第1张图片

      • 初始化:

        minDist数组数值初始化为无穷大,源点(节点1) 到自己的距离为0,所以 minDist[1] = 0

        此时所有节点都没有被访问过,所以 visited数组都为False

        解释:

        再强调一下 minDist数组的含义:记录所有节点到源点的最短路径。

        所以minDist初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。

        示例中节点编号是从1开始,所以为了保持一致,minDist数组下标也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了

      1. 第一步,找到源点到未访问过的结点中距离最近的结点

        (其实就是选出minDist数组中值最小且未访问过的结点,进行访问)

      2. 第二步,该最近节点被标记为访问过

        visited对应值标为True

      3. 第三步,更新非访问节点到源点的距离(即更新minDist数组)

        (即更新从cur出发且直接相连的非访问节点的minDist值)

        设源点s,前两步新访问的最近结点为cur,当前要计算到s距离的非访问结点为某个点node。

        那么相当于,在原来的minDist基础上(作为来源一),

        拓展从新访问的cur结点出发且直接相邻的非访问结点,

        node就属于其中之一,

        现在要去计算拓展到的非访问结点,在cur离源点s距离minDist[cur]的基础上,增加的距离(也就是minDist[cur] + 边cur->node的权重/距离)(来源二),

        这样计算到的新距离,要与原来对应的minDist[node]对比,取更小的进行更新。

        也就是说:令边cur->node的权重为val,

        minDist[node] = min(minDist[node], minDist[cur] + val)

        注意: 这里与prim不一样的地方在于,求源点s到node的距离,该两点是可以非直接相连的!每次求的都是新访问结点之后,最短的源点s到node的距离。

      • 最后的判断:

        要判断是否从s出发抵达了t,只需要检查minDist[t]是否仍为无穷大即可。

        若仍为无穷大,说明不存在路径;

        否则,存在最短路径且长度为minDist[t]

    • 代码随想录C++代码实现:

      #include 
      #include 
      #include 
      using namespace std;
      int main() {
          int n, m, p1, p2, val;
          cin >> n >> m;
      
          vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));
          for(int i = 0; i < m; i++){
              cin >> p1 >> p2 >> val;
              grid[p1][p2] = val;
          }
      
          int start = 1;
          int end = n;
      
          // 存储从源点到每个节点的最短距离
          std::vector<int> minDist(n + 1, INT_MAX);
      
          // 记录顶点是否被访问过
          std::vector<bool> visited(n + 1, false);
      
          minDist[start] = 0;  // 起始点到自身的距离为0
      
          for (int i = 1; i <= n; i++) { // 遍历所有节点
      
              int minVal = INT_MAX;
              int cur = 1;
      
              // 1、选距离源点最近且未访问过的节点
              for (int v = 1; v <= n; ++v) {
                  if (!visited[v] && minDist[v] < minVal) {
                      minVal = minDist[v];
                      cur = v;
                  }
              }
      
              visited[cur] = true;  // 2、标记该节点已被访问
      
              // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组)
              for (int v = 1; v <= n; v++) {
                  if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {
                      minDist[v] = minDist[cur] + grid[cur][v];
                  }
              }
      
          }
      
          if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点
          else cout << minDist[end] << endl; // 到达终点最短路径
      
      }
      
  • 个人python代码实现:

    • 时间复杂度:O(n^2)
    • 空间复杂度:O(n^2)
    import sys
    
    def dijkstra(edges, s, t, n):
        # edges边集合,源点结点s(source),目标结点t(target)
        # 结点总个数n
        # 返回s到t的最短路径长度
        m = len(edges) # 边数
    
        # 创建图(邻接矩阵)
        graph = [[0] * (n + 1) for _ in range(n + 1)] # 结点编号1~n
        for v1, v2, val in edges:
            graph[v1][v2] = val # v1 -> v2,权重为val
    
        # 初始化
        minDist = [float('inf')] * (n + 1) # 结点编号1~n
        visited = [False] * (n + 1)
        minDist[s] = 0 # 源点s到自己的距离为0
    
        # 迭代求s到t最短路径长度
        for i in range(n): #迭代n次,因为需要访问所有n个结点(因为可能不存在路径!必须要先求完整的dijkstra,不能在访问到终点t的时候直接返回,完整更新结束之后,要利用更新后的dijkstra判断是否能够抵达)
            # 第一、二步,找到minDist中的最小值对应结点cur(且一定是未访问过),进行访问
            min_dist = float('inf')
            cur = s # 初始化为起点s
            for node in range(1, n + 1):
                if not visited[node] and minDist[node] < min_dist:
                    min_dist = minDist[node]
                    cur = node
            # print(cur)
            visited[cur] = True # 添加cur为已访问
    
            # 第三步,更新从cur出发且直接相连的非访问节点的minDist值
            for node in range(1, n + 1):
                if not visited[node] and graph[cur][node] > 0:
                    # 计算新距离
                    new_dist = minDist[cur] + graph[cur][node]
                    # 比较更新更小距离
                    if new_dist < minDist[node]:
                        minDist[node] = new_dist
                    # print(minDist)
    
        # 判断是否从s抵达了t
        if minDist[t] == float('inf'): return -1
        else: return minDist[t]
    
    
    if __name__ == '__main__':
        # 求最短路(最小权值路、最短抵达时间)
        lines = sys.stdin.readlines()
    
        n, m = map(int, lines[0].strip().split()) # n个车站(结点),m条公路(边)
    
        # 创建边集合edges(这里将dijkstra封装起来,且能实现直接传入边集合求最短距离,所以就不先构建邻接矩阵了
        edges = []
        for i in range(1, len(lines)):
            # s -> e,权重val
            s, e, val = map(int, lines[i].strip().split())
            edges.append((s, e, val)) 
    
        minDistance = dijkstra(edges, 1, n, n) # 起点结点1,终点结点n,结点总个数n
        print(minDistance)
    
  • 拓展一:如果图中边的权值为负数,dijkstra 还合适吗?

    不合适!

    具体示例见代码随想录链接(该示例中,真正最短路径应该是经过了负值的路径,但是dijkstra却没有成功更新minDist)

    对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,但最终会发现只是 拆了东墙补西墙对dijkstra的补充逻辑只能满足某特定场景最短路求解。

    对于求解带有负权值的最短路问题,可以使用 Bellman-Ford 算法(见后序文章)

  • 拓展二:dijkstra与prim算法的区别(摘录修改自代码随想录)

    prim算法博客:图论 15. 最小生成树之prim算法-CSDN博客

    dijkstra的代码看上去和 prim算法很相似,可以说基本上一致。

    唯一区别在 三部曲中的 第三步: 更新minDist数组

    因为​**prim是求 非访问节点到最小生成树的最小距离,而 dijkstra是求 非访问节点到源点的最小距离**​

    • prim 更新 minDist数组的写法:

      for (int j = 1; j <= v; j++) {
          if (!isInTree[j] && grid[cur][j] < minDist[j]) {
              minDist[j] = grid[cur][j];
          }
      }
      

      因为 minDist表示 节点到最小生成树的最小距离,所以 新节点cur的加入,只需要 使用 grid[cur][j] ,grid[cur][j] 就表示 cur 加入生成树后,生成树到 节点j 的距离。

    • dijkstra 更新 minDist数组的写法:

      for (int v = 1; v <= n; v++) {
          if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {
              minDist[v] = minDist[cur] + grid[cur][v];
          }
      }
      

      因为 minDist表示 节点到源点的最小距离,所以 新节点 cur 的加入,需要使用 源点到cur的距离 (minDist[cur]) + cur 到 节点 v 的距离 (grid[cur][v]),才是 源点到节点v的距离。

    此时大家可能不禁要想 prim算法 可以有负权值吗?

    当然可以!

    提示:prim算法只需要将节点以最小权值和链接在一起,不涉及到单一路径。

  • 附代码随想录python题解代码用于比较学习:

    import sys
    
    def dijkstra(n, m, edges, start, end):
        # 初始化邻接矩阵
        grid = [[float('inf')] * (n + 1) for _ in range(n + 1)]
        for p1, p2, val in edges:
            grid[p1][p2] = val
    
        # 初始化距离数组和访问数组
        minDist = [float('inf')] * (n + 1)
        visited = [False] * (n + 1)
    
        minDist[start] = 0  # 起始点到自身的距离为0
    
        for _ in range(1, n + 1):  # 遍历所有节点
            minVal = float('inf')
            cur = -1
    
            # 选择距离源点最近且未访问过的节点
            for v in range(1, n + 1):
                if not visited[v] and minDist[v] < minVal:
                    minVal = minDist[v]
                    cur = v
    
            if cur == -1:  # 如果找不到未访问过的节点,提前结束
                break
    
            visited[cur] = True  # 标记该节点已被访问
    
            # 更新未访问节点到源点的距离
            for v in range(1, n + 1):
                if not visited[v] and grid[cur][v] != float('inf') and minDist[cur] + grid[cur][v] < minDist[v]:
                    minDist[v] = minDist[cur] + grid[cur][v]
    
        return -1 if minDist[end] == float('inf') else minDist[end]
    
    if __name__ == "__main__":
        input = sys.stdin.read
        data = input().split()
        n, m = int(data[0]), int(data[1])
        edges = []
        index = 2
        for _ in range(m):
            p1 = int(data[index])
            p2 = int(data[index + 1])
            val = int(data[index + 2])
            edges.append((p1, p2, val))
            index += 3
        start = 1  # 起点
        end = n    # 终点
    
        result = dijkstra(n, m, edges, start, end)
        print(result)
    

你可能感兴趣的:(小白的代码随想录刷题笔记,Mophead的小白刷题笔记,leetcode,python,代码随想录,图论)