单源最短路

问题描述:在一个有向无环图中,已知每条边长,求出1到n的最短路径,返回1到n的最短路径值。如果1无法到n,输出-1
示例
输入:5,5,[[1,2,2],[1,4,5],[2,3,3],[3,5,4],[4,5,5]]
返回值:9
说明:两个整数n和m,表示图的顶点数和边数。一个二维数组,一维3个数据,表示顶点到另外一个顶点的边长度是多少每条边的长度范围[0,1000]。注意数据中可能有重边
如图所示1-5的最短路径值为9.返回值为9



方法一

思路分析

    由于边的权重值范围不能小于0,因此该题直接想到的是Dijkstra算法,它可以有效处理单源最短路径。该算法的基本思想是每次找到一个未被访问过的节点,加入到已经访问过的节点集合,然后在已经访问过的节点集合中更新源点到其他节点的权值,使之最小,不断地寻找未被访问的节点,直到访问完全部的节点。具体步骤如下:
  • 设置源节点为$v$,其他节点均设置两个数组,分别为$d[i]$表示从源节点到该节点的最短权值,$vist[i]$表示该节点是否被访问过,若$vist[i]=0$,表示未被访问过
  • 初始化$d[i]$的值为无穷大,初始化$vist[i]$的值为0,表示除源节点外其余节点均未被访问过。
  • 首先从未被访问的节点中,找到与源节点权值最小的点$w$,更新其$d[w]$的值,并更新$vist[w]=1$
  • 接着更新点$w$连接的边的未被访问的节点$u$,如果$weight[w][u]+d[w]<d[u]$,则更新$d[u]=weight[w][u]+d[w]$,以及$vist[u]=1$,此时已经访问过的节点有$v,w,u$
  • 每一次新加入的节点作为更新点,以此寻找未被访问过的点,并更新权值
  • 本题求解的是从$1-n$的最短距离,因此当$vist[n]=1$时,程序便可返回权值

图解

输入:5,5,[[1,2,2],[1,4,5],[2,3,3],[3,5,4],[4,5,5]]
  • 定义一个数组 $d$,$d[i]$ 表示从源点 0 到顶点 i 的边的权值,比如这里 $d[2] =2 $。初始状态下,$d[1]=0$,如果源点0到某一个点存在边,则将其记录,如果不存在则记录为
  • 定义一个集合 $S$ 用于存放距源点最短距离已经确定的顶点。初始状态下,$S = { 1 }$,总所有节点的集合为V
具体过程如下:
  1. 首先,以 1 为源点,到 2 的权值是 2,到 4 的权值是 5,其他情况不存在边
  2. 选择最短的一条作为确定找到的最短路径。即到2的权值最小,因此将 2 加入到 $S$ 集合中。此时 $S = { 1,2 }$。
  3. 接下来我们看 $V-S$ 中与 2 连接的点3。由于 $d[2]+wight[2-3] < d[3]$,故将 $d[3]$ 更新为 $d[2]+wight[2-3]=5$
  4. 在 $V-S$ 的顶点集合中,3,4和源点的权值相同,因此首先将 3 加入到 $S$ 集合中,此时 $S = { 1,2,3 }$。
  5. 接下来我们再看 $V-S$ 中与 3连接的点,只有 5 。由于$ d[3] + wight[3-5] < d[5]$,因此将 $d[5] $更新为 $d[3] + wight[3-5]$。
  6. 再从 $V-S$ 集合中选择距离源点最近的顶点4,因此将 4 加入到 $S$ 集合中。此时$ S = {1,2,3,4}$
  7. 直接将最后一个节点5放入$S$中,运行结束
顶点 权值(步骤1、2) 步骤3 步骤4、5、6 步骤7
1 0


2 2



3 5

4 5
5 5
5

9 9
$S=\{1\}$ $\{1,2\}$ $\{1,2,3\}$ $\{1,2,3,4\}$ $\{1,2,3,4,5\}$

动图演示


C++核心代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int 顶点数
     * @param m int 边数
     * @param graph intvector<vector<>> 一维3个数据,表示顶点到另外一个顶点的边长度是多少
     * @return int
     */
    int findShortestPath(int n, int m, vector<vector<int> >& graph) {
        // write code here
    vector<vector<int>> e(n + 1, vector<int>(n + 1, INT_MAX));
    vector<int> d(n + 1, INT_MAX);
    vector<bool> visit(n + 1, false);
    for (auto& g : graph) 
    {
        e[g[0]][g[1]] = min(g[2], e[g[0]][g[1]]); // 处理重边
    }
    d[1] = 0;
    for (int i = 1; i <= n; i++) 
    {
        int u = -1, minn = INT_MAX;
        for (int j = 1; j <= n; j++) 
        {
            if (!visit[j] && d[j] < minn) //该节点没有被访问过,找到当前情况下距离源点最小的节点,目的是为了将该节点加入到已经访问的集合中
            {
                minn = d[j];//记录权值的最小值,
                u = j;
            }
        }
        if (u == -1)//u没有更新,所有节点均已经访问过,程序结束
        {
            break;
        }
        visit[u] = true;//标记u为已经访问过的节点
        for (int v = 1; v <= n; v++)
        {
            if (!visit[v] && e[u][v] != INT_MAX) //新加入已经访问节点作为中间点,更新其他点(未被访问过,并且两个点之间存在边)距离源点的权值,以便下一次访问最小权值的节点
            {
                if (d[v] > d[u] + e[u][v]) 
                {
                    d[v] = d[u] + e[u][v];//更新源点到v的权值
                }
            }
        }
    }
    return d[n] == INT_MAX ? -1 : d[n];
    }
};

复杂度分析

  • 时间复杂度:存在内外两层循环,因此时间复杂度为$O(n*m)$
  • 空间复杂度: 借助与一个辅助数组和一个标记数组以及一个存储边的数组,空间复杂度为$O(n+n+n*n)=O(n^2)$

方法二

思路分析

      根据方法一的思想,本题还可采用动态规划的办法,依次遍历每一个节点,遍历每一条边,不断的更新当前点到源点的权值,当前点距离源点的权值初始化为最大数,则其公式表达式与前一节点距离源点有关。遍历的过程中不断的更新权值,因此有如下表达式:
$temp[i+1]=min(temp[i]+wight[i+1,i],temp[i+1])$

图解

输入:5,5,[[1,2,2],[1,4,5],[2,3,3],[3,5,4],[4,5,5]]


C++核心代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int 顶点数
     * @param m int 边数
     * @param graph intvector<vector<>> 一维3个数据,表示顶点到另外一个顶点的边长度是多少
     * @return int
     */
    int findShortestPath(int n, int m, vector<vector<int> >& graph) {
        // write code here
        vector<int> temp(n,INT_MAX);
        temp[0]=0;//设置源点到自身的值为0
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                if(i+1 == graph[j][0])//第i个点到第i+1个点的值
                {
                    int path = INT_MAX == temp[i] ? INT_MAX : graph[j][2]+temp[i];//节点graph[j][1]距离源点可能的权值
                    temp[graph[j][1] - 1] = min(temp[graph[j][1] - 1], path);//防止有重边的出现
                }
            }
        }
        return temp[n - 1] == INT_MAX ? -1 :temp[n - 1];
    }
    int min(int a,int b)
    {
        return a<=b?a:b;
    }
};

复杂度分析

  • 时间复杂度:两层循环遍历,外层遍历次数为$n$,内层遍历次数为$m$,因此时间复杂度为$O(n*m)$
  • 空间复杂度:  借助于一个辅助数组,用于存放其他点到源点的最小权值,因此复杂度为$O(n)$