图论最短路之Dijkstra算法,SPFA算法

Dijkstra算法的几个关键点:

一.最短路径的最优子结构性质(转载 原文链接http://www.cnblogs.com/dolphin0520/archive/2011/08/26/2155202.html)

    该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。

    假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。

二.vis[i]代表是否已确定找到了从起点到i的最短路径

三.在n-1次循环中去找除了起点以外的n-1个点的最短路径,每一次循环确定一个,那为什么每次在所有还未确定最短路径的点中距离最短的,就一定是已经找到了从起点到该点的最短路径的点呢?

    反证法:1.在第一次循环中,假设在所有vis为false的点中dist[V1]最短,即起点V0到V1的距离最短,从起点到其他点(如V5)再到V1,V0V5>V0V1,那V0V5V1就更大于V0V1,则dist[V1]已经为最短路径,vis[V1]可以置为true;
                 2.同理可得,在某一次循环中,在所有vis为false的点中dist[V2]最短,如果绕过其他点如V3到V2距离更短,即dist[V3]+V3V2<dist[V2],则dist[v3]<dist[V2],这与dist[V2]最短相矛盾。换句话说,dist[V3]就已经比dist[V2]大了,  从V0绕过V3再到V2的距离肯定比dist[V2]要长,所以此时dist[V2]就是从V0到V2的最短路径。

四.为什么每次循环中只拿当前确定最短路径的点去更新?

    准确的说应该是拿当前确定的最短路径这一段从起点到该点的最优路径,去更新其它还未确定最短路径的点到起点的距离(松弛操作)。因为是滚动比较的,这一次比较就是和历史最优的记录进行比较。

    因为最优子结构!

五.Dijkstra算法处理负权有向图可能会出错

比如下图

    用Dijkstra求得d[1,2]=3,事实上d[1,2]=2。这是因为Dijkstra算法每次将离起点最近的点视为是已经找到最短路的点,并拿它去松弛其他还未找到最短路的点,而不再更新自己的最短路。这就是为什么Dijkstra算法要快一点,因为它每次只拿当前新增的已找到最短路的点去松弛其他点,相对SPFA来说减少了很多冗余的松弛操作。但这也就是为什么Dijkstra算法不能处理负权有向图,原因如上图所示,此时离起点最近的点不一定就是已经找到最短路的点,这个点还有可能可以被负权更新成更短的路径。

    对于负权有向图可以使用SPFA来处理。SPFA虽然有些冗余:只要被松弛了而且不在队列里面就进队列,因为此时这个被松弛了的点可能可以松弛其他点,只要有可能就将其进队列,待以后出队列的时候就尝试松弛其他所有和这个点有边相连的点。虽然SPFA比Dijkstra慢了点,但可以正确地处理有边权为负的情况(此时一定为有向图,若为无向图且有负权边肯定形成了负环,有负环就可以一直在负环里转,此时无解)

六、Dijkstra算法与SPFA算法(hdu 1874

#include <bits/stdc++.h>

using namespace std;

#define ll long long
#define N 1005
#define mod 1000000007
#define INF 0x3f3f3f3f

const double eps=1e-8;
const double pi=acos(-1.0);

int mp[N][N];
int dist[N];
int vis[N];
int n,m;

// 错误版本
void minD_v1(int v0)
{
    memset(vis, 0, sizeof(vis));
    memset(dist, INF, sizeof(dist));
    dist[v0]=0;

    queue<int> q;
    q.push(v0); vis[v0]=1;
    while(!q.empty()){
        int v=q.front(); q.pop();
        printf("***v=%d***\n", v);
        for(int i=0;i<n;i++){
            if(mp[v][i]!=INF){
                dist[i]=min(dist[i], dist[v]+mp[v][i]); // 更新时不能保证dist[v]是v0到v的最短距离
                printf("dist(%d) = %d\n", i, dist[i]);
                if(!vis[i]) {q.push(i); vis[i]=1;}
            }
        }
    }
}
/* minD_v1错误样例:
4 4
0 1 5
0 2 1
2 1 2
1 3 2
0 3
*/

// 正确版本,Dijkstra算法
void minD_v2(int v0)
{
    memset(vis, 0, sizeof(vis));
    memset(dist, INF, sizeof(dist));
    for(int i=0;i<n;i++) if(mp[v0][i]!=INF) dist[i]=mp[v0][i];

    vis[v0]=1; // vis代表找到到该顶点的最短路了
    for(int i=0;i<n-1;i++){
        int Min=INF, u=-1;
        for(int j=0;j<n;j++){
            // 在未找到最短路的顶点集合S1中,距源点距离最小的顶点u一定是新进的已经找到最短路的顶点,
            // 因为此时从源点通过S1中任何其他点v间接到达u的距离一定会比当前的dist[u]大,
            // 即dist[v]+mp[v][u]>dist[u]对S1中的任何v(v!=u)均成立,因为dist[v]>=dist[u],且mp[v][u]>0
            // 于是u一定是已经找到最短路的顶点,
            // 但当mp[v][u]<0时,上面的等式不一定成立,于是Dijkstra算法不适合带有负权边的图
            if(!vis[j]&&Min>dist[j]){
                Min=dist[j];
                u=j;
            }
        }
        if(u==-1) break; // 剩下顶点到源点距离均为INF,此时不用再更新
        vis[u]=1;
        for(int j=0;j<n;j++){
            if(mp[u][j]!=INF&&!vis[j]){
                if(dist[j]>dist[u]+mp[u][j])
                    dist[j]=dist[u]+mp[u][j];
            }
        }
    }
}

// 正确版本,SPFA算法
void minD_v3(int v0)
{
    memset(vis, 0, sizeof(vis));
    memset(dist, INF, sizeof(dist));
    dist[v0]=0;

    queue<int> q;
    q.push(v0); vis[v0]=1;// 代表在队列中
    while(!q.empty()){
        int v=q.front(); q.pop(); vis[v]=0;
        for(int i=0;i<n;i++){
            if(mp[v][i]!=INF){
                if(dist[i]>dist[v]+mp[v][i]){
                    dist[i]=dist[v]+mp[v][i];
                    // 当dist[i]被更新后,只要i不在队列中就重新进入队列,
                    // 因为最短路减小了的i有可能可以更新由它连接的顶点的最短路
                    // 那没被更新的顶点就不用放进队列吗?
                    // 是的,因为刚开始除了v0所有点的dist均为INF,
                    // 故所有从源点可达的顶点都会至少进入一次队列、至少尝试更新一次由它连接的顶点的最短路,
                    // 所以当某个顶点未被更新时,它就不可能让由它连接的顶点的最短路更小,故不用进队列
                    if(!vis[i]) q.push(i), vis[i]=1;
                }
            }
        }
    }
}

int main()
{
    while(cin>>n>>m){
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                mp[i][j]=(i==j?0:INF);

        for(int i=0;i<m;i++){
            int a,b,w;
            cin>>a>>b>>w;
            mp[b][a]=mp[a][b]=min(mp[a][b],w); //重边取最小,mp[a][b]与mp[b][a]捆绑赋值
        }

        int s,t; cin>>s>>t;
        minD_v3(s);
        printf("%d\n", dist[t]==INF?-1:dist[t]);
    }
    return 0;
}

 

七、Dijkstra算法与SPFA算法的其他写法

SPFA算法(适用无负权双向/无向图,负权有向图),图的 边vector结构 存储。

参考qsc大神直播的代码,视频链接,温馨提醒:视频里有些小错误,qsc大神已经在评论区纠正了

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#include <vector>
#include <queue>
#include <stack>
#include <set>
#include <map>

using namespace std;

#define FOR(i,k,n) for(int i=k;i<n;i++)
#define FORR(i,k,n) for(int i=k;i<=n;i++)
#define scan(a) scanf("%d",&a)
#define scann(a,b) scanf("%d%d",&a,&b)
#define scannn(a,b,c) scanf("%d%d%d",&a,&b,&c)
#define mst(a,n)  memset(a,n,sizeof(a))
#define ll long long
#define N 200005
#define mod 1000000007
#define INF 0x3f3f3f3f

const double eps=1e-8;
const double pi=acos(-1.0);

vector<pair<int,int> > E[N];
int d[N];
int inque[N];

int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);

    int n,m;
    while(cin>>n>>m)
    {
        mst(d,INF);
        mst(inque,0);
        FOR(i,0,N) E[i].clear();
        FOR(i,0,m)
        {
            int a,b,c;
            cin>>a>>b>>c;
            E[a].push_back(make_pair(b,c));
            //E[b].push_back(make_pair(a,c));
        }
        queue<int> q;
        int s=1;
        q.push(s); inque[s]=1; d[s]=0;
        while(!q.empty())
        {

            int cur=q.front();
            q.pop(); inque[cur]=0;
            //printf("**当前出队列点:%d\n",cur);
            for(int i=0;i<E[cur].size();i++)
            {
                int v=E[cur][i].first;
                //printf("松弛%d\n",v);
                if(d[v]>d[cur]+E[cur][i].second)
                {
                    d[v]=d[cur]+E[cur][i].second;
                    if(!inque[v]) q.push(v),inque[v]=1;
                }
            }
        }
        FORR(i,2,n) printf("%d\n",d[i]);
    }
    return 0;
}

 

用优先队列(堆)优化的Dijkstra算法

按qcs大神的这种写法,好像也可以解决负权有向图最短路问题?(参考qsc大神直播的代码,视频链接,温馨提醒:视频里有些小错误,qsc大神已经在评论区纠正了)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
#include <vector>
#include <queue>
#include <stack>
#include <set>
#include <map>

using namespace std;

#define FOR(i,k,n) for(int i=k;i<n;i++)
#define FORR(i,k,n) for(int i=k;i<=n;i++)
#define scan(a) scanf("%d",&a)
#define scann(a,b) scanf("%d%d",&a,&b)
#define scannn(a,b,c) scanf("%d%d%d",&a,&b,&c)
#define mst(a,n)  memset(a,n,sizeof(a))
#define ll long long
#define N 200005
#define mod 1000000007
#define INF 0x3f3f3f3f

const double eps=1e-8;
const double pi=acos(-1.0);

vector<pair<int,int> > E[N];
int d[N];
int vis[N];

int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);

    int n,m;
    while(cin>>n>>m)
    {
        mst(d,INF);
        mst(vis,0);
        FOR(i,0,N) E[i].clear();
        FOR(i,0,m)
        {
            int a,b,c;
            cin>>a>>b>>c;
            E[a].push_back(make_pair(b,c));
            E[b].push_back(make_pair(a,c));
        }
        int s,t;
        cin>>s>>t;
        priority_queue<pair<int,int> > q;
        d[s]=0;
        q.push(make_pair(-d[s],s));
        while(!q.empty())
        {
            int cur=q.top().second;
            q.pop();
            if(vis[cur]) continue;//已经找到最短路却被pop出来,说明这个点之前被已经找到最短路的其他点松弛了一些,所以被push了进去
            //但那时候并没有松弛到最短,那么这个点第一次被pop出来的时候(已经松弛到最短)就已经用这个点的最短路松弛过其他点了,不需要重复

            vis[cur]=1;//已经找到最短路
            for(int i=0;i<E[cur].size();i++)
            {
                int v=E[cur][i].first;
                if(!vis[v]&&d[v]>d[cur]+E[cur][i].second)
                {
                    d[v]=d[cur]+E[cur][i].second;
                    q.push(make_pair(-d[v],v));
                }
            }
        }
        if(d[t]!=INF) printf("%d\n",d[t]);
        else printf("-1\n");
    }
    return 0;
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值