最短路问题--最通俗易懂的讲解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sugarbliss/article/details/86511043

最短路径的定义:

所谓最短路径是指:如果从图中某一顶点(源点)到达另一顶点(终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边的权值总和(称为路径长度)达到最小。  

可以将适用最短路的算法分为单源最短路,和多源最短路如下图:

多源最短路算法Floyd:

 Floyd-Warshall 算法用来找出每对点之间的最短距离。它需要用邻接矩阵来储存边,这个算法通过考虑最佳子路径来得到最佳路径。 注意单独一条边的路径也不一定是最佳路径。

Floyd算法的基本思想如下:

从任意节点 i 到任意节点 j 的最短路径不外乎两种可能,一是直接从 i 到 j ,二是从 i 经过若干个节点 k 到 j 。所以,我们假设d(i,j)为节点 i 到节点 j 的最短路径的距离,对于每一个节点k,我们检查d(i,k) + d(k,j)  <  d(i,j)是否成立,如果成立,证明从 i 到 k 再到 j 的路径比 i 直接到 j 的路径短,我们便更新d(i,j) = d(i,k) + d(k,j),这样一来,当我们遍历完所有节点k,d(i,j)中记录的便是 i 到 j 的最短路径的距离。

核心代码:

for(int k = 1; k <= n; k++)
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            d[i][j] = min(d[i][j], d[i][k] + d[k][j]);

从动态规划角度理解Floyd:

动态转移的基本思想可以认为是建立起某一状态和之前状态的一种转移表示。

把d[k][i][j]定义成:只能使用第1号到第k号点作为中间媒介时,点i到点j之间的最短路径长度。d[k][i][j]是一种使用1号到k号点的状态,可以想办法把这个状态通过动态转移,规约到使用1号到(k-1)号的状态,即d[k-1][i][j]。对于d[k][i][j](即使用1号到k号点中的所有点作为中间媒介时,i和j之间的最短路径)。

可以分为两种情况:(1)i到j的最短路不经过k;(2)i到j的最短路经过了k。

不经过点k的最短路情况下,d[k][i][j] = d[k-1][i][j]。

经过点k的最短路情况下,d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]。

因此,综合上述两种情况,便可以得到Floyd算法的动态转移方程:d[k][i][j] = min(d[k-1][i][j], d[k-1][i][k]+d[k-1][k][j])。

那么经过滚动优化Floyd算法的动态转移方程:d[i][j] = min(d[i][j], d[i][k]+d[k][j])。

三层for循环,时间复杂度O(n3),可以解决负权边,可以求任意两点的最短距离,也可以解决传递闭包问题:

简单说一下传递闭包:对于一个节点i,如果j能到i,i能到k,那么j就能到k,计算完成后,我们可以判断任意两个点是否相连。

代码如下:

    for(int k = 1; k <= n; k++)
    {
        for(int i = 1; i <= n; i++)
        {
            for(int j = 1; j <= n; j++)
            {
                if(G[i][j] == 1 || G[i][k] == 1 && G[k][j] == 1)
                    G[i][j] = 1; //标记为1,代表i,j这两个节点相连
            }
        }
    }

 Floyd算法的模板:

#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1000;
const int INF = 0x3f3f3f3f;
int dis[N], G[N][N], n, s;
bool vis[N];
void floyd()
{
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                G[i][j] = min(G[i][j], G[i][k]+G[k][j]);
    printf("%d\n", G[1][n]);
}
int main()
{
    int m, e, w;
    while(~scanf("%d%d", &n, &m) &&(n+m))
    {
        memset(vis, 0, sizeof(vis));
        for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
        {
            if(i == j) G[i][j] = 0;
            else G[i][j] = INF;
        }
        for(int i = 1; i <= m; i++)
        {
            scanf("%d%d%d", &s, &e, &w);
            G[s][e] = G[e][s] = w;
        }
        floyd();
    }
    return 0;
}

单源最短路Dijkstra​​​​​​

 Dijkstra​​​​​​是基于贪心策略的思想,算法是按路径长度从小到大的顺序一步一步并入来求取,用来解决单源点到其余顶点的最短路径问题。时间复杂度O(N2),由于它是按路径​​​​从小到大来求最短路,所以我们可以用堆优化,堆优化后时间复杂度O(nlogn)。

算法的过程是:每次找到离源点(上面例子的源点就是1号顶点)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。

总结一下就是:从剩余路径中找最短的路径,然后更新最短路径。

为什么Dijkstra​​​​​​不能解决带有负权的边:因为dijkstra是基于贪心策略,每次都找一个距源点最近的点,然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点,再通过这个负权边,使得路径之和更小,这样就出现了错误。

Dijkstra算法和Prim算法非常相似,我来说一下它们的区别:

Dijkstra算法用于构建单源点的最短路径树(MST)——即树中指定点到任何其他点的距离都是最短的。例如,构建地图应用时查找自己的坐标离北京,上海,郑州等的最短距离。可以用于 有向图  ,但是不能存在负权值(Spfa可以处理负权值)。

Prim算法用于构建最小生成树——即树中所有路径之和最小,但不能保证任意两点之间是最短路径。例如,构建电路板,使所有边的和花费最少。只能用于无向图。

它们的区别只在于最后循环体内的松弛操作,简单总结就是,Dijkstra的松弛操作加上了到起点的距离,而Prim只有相邻节点的权值。

Dijkstra堆优化+链式前向星模板:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 2e5+7;
using namespace std;
struct node {int to,w,next;} edge[maxn];
int head[maxn], cnt;
int dis[maxn], vis[maxn];
int n, m, s, t;
struct Dijkstra
{
    void init()
    {
        memset(head,-1,sizeof(head));
        memset(dis,0x3f,sizeof(dis));
        memset(vis,0,sizeof(vis));
        cnt = 0;
    }
 
    void add(int u,int v,int w)
    {
        edge[cnt].to = v;
        edge[cnt].w = w;
        edge[cnt].next = head[u];
        head[u] = cnt ++;
    }
 
    void dijkstra()
    {
        priority_queue<pii,vector<pii>,greater<pii> > q;//从小到大
        dis[s] = 0; q.push({dis[s],s});
        while(!q.empty())
        {
            int now = q.top().second;
            q.pop();
            if(vis[now]) continue;
            vis[now] = true;
            for(int i = head[now]; i != -1; i = edge[i].next)
            {
                int v = edge[i].to;
                if(!vis[v] && dis[v] > dis[now] + edge[i].w)
                {
                    dis[v] = dis[now] + edge[i].w;
                    q.push({dis[v],v});
                }
            }
        }
    }
}dj;
 
int main()
{
    while(~scanf("%d%d",&n,&m) && n+m)
    {
        dj.init();
        for(int i = 0; i < m; i++)
        {
            int u, v, w;
            scanf("%d%d%d",&u, &v, &w);
            dj.add(u, v, w);
            dj.add(v, u, w);
        }
        s = 1, t = n;//s起点,t终点
        dj.dijkstra();
        printf("%d\n",dis[t]);
    }
}

单源最短路Spfa

Spfa就是使用队列或者栈的Bellman-Ford算法的优化版本,所以我们直接说Spfa。

若给定的图存在负权边,类似Dijkstra​​​​​​等算法便没有了用武之地,Spfa算法便派上用场了。

算法思想:简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。                                                                                                                                                                                                                                                                        -------摘自百度百科

Spfa算法用一句话概括就是:对所有边进行n-1次松弛操作,它可以处理负边,判断负环,该算法的最坏复杂度为 O(VE)。

为什么Spfa可以处理负边因为在Spfa中每一个点松弛过后说明这个点距离更近了,所以有可能通过这个点会再次优化其他点,所以将这个点入队再判断一次。

如何判断成环: 在储存边时,记录下每个点的入度,每个点入队的时候记录一次,如果入队的次数大于这个点的入度,说明从某一条路进入了两次,即该点处成环。简单来说就是如果一个点进入队列达到n次,则表明图中存在负环。

Spfa算法和BFS算法非常相似,我来说一下它们的区别:它们的区别是BFS中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点松弛过其它的点之后,过了一段时间可能本身被松弛,于是要再次用来松弛其它的点,这样反复迭代下去。

Spfa算法+链式前向星模板

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 2e5+7;
using namespace std;
struct node {int to,w,next;} edge[maxn];
int head[maxn], cnt;
int dis[maxn], vis[maxn];
int n, m, s, t;
struct Spfa
{
    void init()
    {
        memset(head,-1,sizeof(head));
        memset(dis,0x3f,sizeof(dis));
        memset(vis,0,sizeof(vis));
        cnt = 0;
    }
 
    void add(int u,int v,int w)
    {
        edge[cnt].to = v;
        edge[cnt].w = w;
        edge[cnt].next = head[u];
        head[u] = cnt ++;
    }
 
    void spfa()
    {
        dis[s] = 0; vis[s] = 1;
        queue <int> Q; Q.push(s);
        while(!Q.empty())
        {
            int now = Q.front();
            Q.pop(); vis[now] = 0;    //从队列中弹出now , vis[now] 标记为未访问
            for(int i = head[now]; i != -1; i = edge[i].next)
            {
                int v = edge[i].to;
                if(dis[v] > dis[now] + edge[i].w)
                {
                    dis[v] = dis[now] + edge[i].w;
                    if(vis[v]) continue;    //如果访问过了(也就是 已经在队列中),就不用再push
                    vis[v] = 1; Q.push(v);
                }
            }
        }
    }
}sp;
 
int main()
{
    while(~scanf("%d%d",&n,&m) && n+m)
    {
        sp.init();
        for(int i = 0; i < m; i++)
        {
            int u, v, w;
            scanf("%d%d%d",&u, &v, &w);
            sp.add(u, v, w);
            sp.add(v, u, w);
        }
        s = 1, t = n; //s起点,t终点
        sp.spfa();
        printf("%d\n", dis[t]);
    }
}

注意:如果存在从源点可达的负权值回路,则最短路径不存在,因为可以重复走这个回路,使得路径无穷小。

参考:https://www.cnblogs.com/chenying99/p/3932877.html
           https://www.jianshu.com/p/92e46d990d17

猜你喜欢

转载自blog.csdn.net/sugarbliss/article/details/86511043
今日推荐