首页 > 编程语言 > 详细

最短路算法的总结

时间:2020-12-08 09:42:09      阅读:26      评论:0      收藏:0      [点我收藏+]

因为各种原因鸽了一个月的博客。今天就把三种最短路都简单归纳一下记录一下。

首先介绍最短路的背景

  最短路问题是图论理论的一个经典问题。寻找最短路径就是在指定网络中两结点间找一条距离最小的路。最短路不仅仅指一般地理意义上的距离最短,还可以引申到其它的度量,如时间、费用、线路容量等。
最短路径算法的选择与实现是通道路线设计的基础,最短路径算法是计算机科学与地理信息科学等领域的研究热点,很多网络相关问题均可纳入最短路径问题的范畴之中。经典的图论与不断发展完善的计算机数据结构及算法的有效结合使得新的最短路径算法不断涌现。对最短路问题的研究早在上个世纪60年代以前就卓有成效了,其中对赋权图的有效算法是由荷兰著名计算机专家E.W.Dijkstra在1959年首次提出的,该算法能够解决两指定点间的最短路,也可以求解图G中一特定点到其它各顶点的最短路。后来海斯在Dijkstra算法的基础之上提出了海斯算法。但这两种算法都不能解决含有负权的图的最短路问题。因此由Ford提出了Ford算法,它能有效地解决含有负权的最短路问题。
 
1.Floyd算法
floyd可以在O(n^3)的时间复杂度,O(n^2)的空间复杂度下求解正权图中任意两点间的最短路长度.它的本质是动态规划.
定义f[k][i][j]表示从i出发,途中只允许经过编号小于等于k的点时的最短路.(i,j可以大于k但i到j的路径上的其他点必须编号小于等于k).
转移时从第k层的DP数组f[k][][]求解第k+1层的DP数组f[k+1][i][j].我们可以将f[k+1][][]全部初始化为inf
一条路径如果保证中转的点编号小于等于k,那么一定也满足经过的点的编号小于等于k+1.于是可以先将上一层的dp数组直接复制到第k+1层,f[k+1][i][j]=f[k][i][j].
接下来考虑经过了第k+1个点作为中转点的最短路.我们枚举(i,j),i!=k+1,j!=k+1,然后令f[k+1][i][j]=min(f[k+1][i][j],f[k][i][k+1]+f[k][k+1][j]).
直接这么写的空间复杂度是O(n^3),接下来我们把空间压到O(n^2).i,j这两维都是压不掉的,所以我们把k这一维压掉.f[i][j]现在存的是f[k][i][j].接下来我们把f[i][j]进行更新使得它里面的数值变为f[k+1][i][j]
代码实现也很简单
#include<bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 100;
int map1[maxn][maxn];
int u,v,w;
int main(){
    int n,m,i,j,k;
    memset(map1,inf,sizeof(map1));
    for(i = 1;i<=n;i++){
        map1[i][i] = 0;
    } 
    while(m--){
        scanf("%d %d %d",&u,&v,&w);
        map1[u][v] = map1[v][u] = w;
    }
    for(k = 1;k<=n;k++){
        for(i = 1;i<=n;i++){
            for(k = 1;k<=n;k++){
                if(map1[i][j]>map1[i][k]+map1[k][j]){
                    map1[i][j]>map1[i][k]+map1[k][j];
                }
            }
        }
    }
    printf("%d\n",map1[1][n]);
} 

这里选择了使用邻接矩阵存图,原因是Floyd算法复杂度较高,一般用于解决规模较小的问题,所以不需要更高效率的存储工具,邻接矩阵可以很好的完成这个任务。

2.dijstra算法

Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

技术分享图片

 

 

算法的思路也很清晰,我们先尝试用邻接矩阵来实现这个算法

#include<bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 1000;
int map1[maxn][maxn],dis[maxn];
bool vis[maxn]={false};
int u,v,w,n;
void Dijkstra(int start)
{    
    vis[start]=true;
    for(int i=0;i<n;i++)
        dis[i]=map1[start][i];
        dis[start]=0;
    for(int i=0;i<n;i++)//进行n次操作,i无实际意义 
    {
        int min1=inf;//最小值记录 
        int now=-1;//当前最短的点
        for(int j=0;j<n;j++)//找到当前最短的dis[j] 
        {
            if(!vis[j]&&dis[j]<min1)
            {
                min1=dis[j];
                now=j;        
            }    
        } 
        if(now==-1)
            return ;
        vis[now]=true;
        for(int j=0;j<n;j++)
        {
            if(!vis[j]&&map1[now][j])
            {
                if(dis[j]>map1[now][j]+dis[now])
                    dis[j]=map1[now][j]+dis[now];
            }    
        } 
    }
}
int main()
{
    int m,s;
    cin>>n>>m>>s;//n个点,m条变,从s开始 
    for(int i=0;i<maxn;i++)
    for(int j=0;j<maxn;j++)
    map1[i][j]=inf;
    for(int i=0;i<m;i++)
    {
        cin>>u>>v>>w;
        map1[u][v]=w;
    }
    Dijkstra(s);//从s开始 
    for(int i=0;i<n;i++)
    {
        cout<<dis[i]<<endl;
    } 
    return 0;
} 

可以发现这个算法的复杂度是O(n2)的虽然已经很优秀了,但是在面对一些更复杂问题的时候,这个复杂度还不够理想,于是我们想优化这个算法首先可以从空间上优化,我们可以用vector模拟链表存储从每个点出发的所有边的终点和权值。然后用优先队列维护每次选中的最短边。用vis或if (d[v] < p.first) continue;来防止多次重复,按bfs的思想结合优先队列,可以让这个算法的复杂度降低到O(nlogn)基本可以满足所有问题,就算求多源最短路,我们只用跑n次就能做到比Floyd优秀。

#include<bits/stdc++.h>
using namespace std;
int s, t, n, m, q;
const int MAXV = 100005;
struct edge
{
    int to, cost;
};
typedef pair<int, int> P; //first是最短距离,second是顶点的编号
int V;//顶点个数
vector<edge> G[MAXV];
int d[MAXV];
int vis[MAXV];
void Dijkstra(int s)
{
    priority_queue<P, vector<P>, greater<P> > que;
    for(int i = 1; i <= 100000; i++)
        d[i] = 2000000000;
    d[s] = 0;
    que.push(P(0, s)); //把起点推入队列
    while(!que.empty())
    {
        P p = que.top();
        que.pop();
        int v = p.second; //顶点的编号
        /*if(vis[v])continue;//防止节点的重复扩展
        vis[v]=1;*/
        if (d[v] < p.first) continue;
        for(int i = 0; i < G[v].size(); i++)
        {
            edge e = G[v][i];
            if (d[e.to] > d[v] + e.cost)
            {
                d[e.to] = d[v] + e.cost;
                que.push(P(d[e.to], e.to));
            }
        }
    }
}
int main()
{
    cin >> n >> m >> s;
    for(int i = 1; i <= n; i++)
        G[i].clear();
    edge temp;
    for(int i = 1; i <= m; i++)
    {
        cin >> q >> temp.to >> temp.cost;
        G[q].push_back(temp);
    }
    Dijkstra(s);
    for(int i = 1; i <= n; i++)
    {
        if(d[i] != 2000000000)cout << d[i] << " ";
        else cout << 2147483647 << " ";
    }
    cout << endl;
    return 0;
}

如果追求更优秀的效率,那么用链式前向星存图大约可以优化五分之一左右的常数,代码也附在楼下

#include<bits/stdc++.h>

const int MaxN = 100010, MaxM = 500010;

struct edge
{
    int to, dis, next;
};

edge e[MaxM];
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;

inline void add_edge( int u, int v, int d )
{
    cnt++;
    e[cnt].dis = d;
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt;
}

struct node
{
    int dis;
    int pos;
    bool operator <( const node &x )const
    {
        return x.dis < dis;
    }
};

std::priority_queue<node> q;

inline void dijkstra()
{
    dis[s] = 0;
    q.push( ( node )
    {
        0, s
    } );
    while( !q.empty() )
    {
        node tmp = q.top();
        q.pop();
        int x = tmp.pos, d = tmp.dis;
        if( vis[x] )
            continue;
        vis[x] = 1;
        for( int i = head[x]; i; i = e[i].next )
        {
            int y = e[i].to;
            if( dis[y] > dis[x] + e[i].dis )
            {
                dis[y] = dis[x] + e[i].dis;
                if( !vis[y] )
                {
                    q.push( ( node )
                    {
                        dis[y], y
                    } );
                }
            }
        }
    }
}


int main()
{
    scanf( "%d%d%d", &n, &m, &s );
    for(int i = 1; i <= n; ++i)dis[i] = 0x7fffffff;
    for( register int i = 0; i < m; ++i )
    {
        register int u, v, d;
        scanf( "%d%d%d", &u, &v, &d );
        add_edge( u, v, d );
    }
    dijkstra();
    for( int i = 1; i <= n; i++ )
        printf( "%d ", dis[i] );
    return 0;
}

3.spfa算法

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

很多时候我们并不需要那么多无用的松弛操作。这时候就不再看边了,而是看点,如果dis[i]刷新了,那么说明这个i点可以为我们接下来的松弛提供帮助,我们应该将它记录起来。

那么我们用队列来维护“可能会引起松弛操作的点”,就可以只访问必要的边了,从而省去没必要的操作。

还要注意一点,这个队列里的元素是可以重复入队的,因为一个点的最短路径可以不断更新。另外,当一个点已经在队列里的时候,我们是不用将它入队的,因为我们记录这个点的目的是等会要使用它,所以我们记的是它的编号。     这里一定要和Dij的堆优化分清楚,Dij堆优化的队列里面存的是路径长度,我们这里不需要,所以就算这个点的最短路径中间可能会变,但是不影响我们的结果,我们只需要记住这个点可能可以作为松弛的中转点,也就是我们可以用它进行松弛,就够了。

但是这个算法的问题在于,面对菊花图的数据,它的复杂度会降低到O(n2),所以如果没有负边权的情况下,最好不要使用

代码如下

#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
struct node
{
    int v, w, next;
}edge[5000005];
int n, tot;
int head[1000005], dis[1000005];
bool vis[1000005];
void add_edge(int u,int v,int w)
{
    tot++;
    edge[tot].v = v;
    edge[tot].w = w;
    edge[tot].next = head[u];
    head[u]=tot;
}
void spfa(int x)
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        dis[i] = INF;
        vis[i] = 0;
    }
    dis[x] = 0;
    vis[x] = 1;
    q.push(x);
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(int i=head[u]; i; i=edge[i].next)
        {
            int v = edge[i].v;
            if(dis[v] > dis[u]+edge[i].w)
            {
                dis[v] = dis[u]+edge[i].w;
                if(!vis[v])
                {
                    vis[v] = 1;
                    q.push(v);
                }
            }
        }
    }
}
int main()
{
    int m;
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin >> n >> m;
    tot = 0;
    while(m--)
    {
        int u, v;
        cin >> u >> v;
        add_edge(u, v, 1);
        add_edge(v, u, 1);
    }
    spfa(1);
    for(int i=1; i<=n; i++)
        cout << i << " " << dis[i] << endl;
    return 0;
}

总结,实际上关于什么时候选取那个最短路路算法,很容易就能看出来,如果数据规模小且是多源最短路,那么无论如何都可以选取Floyd算法。如果有负边权而且数据范围较大,用spfa算法,其他情况都可以用dijkstar算法解决,不管是多源最短路还是单源最短路。他都是稳定且优秀的最佳算法。

 

 

 

 

 

最短路算法的总结

原文:https://www.cnblogs.com/tscjj/p/14100659.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!