【算法】单源最短路径的Dijkstra、Bellman-Ford及Spfa算法对比及例题

Dijkstra

dijkstra算法本质上应该是贪心的思想。
从初始节点开始,首先往队列中加入初始节点可以到达的节点的长度,选取最短的一条作为新的节点,然后用该节点更新到达剩余节点的距离,选取最短的距离…如此循环,直到所有点都被确定。
此方法中,每次节点被选中,那么他到达起点的最短距离也就确定了,后续不会再更新了,因为你每一步取的都是当前最小的距离。
dijkstra就是这样不断从剩余节点中拿出一个可以确定最短路径的节点,最终求得从起点到每个节点的最短距离。

例题:

洛谷P4779 【模板】单源最短路径(标准版)

#include<bits/stdc++.h>
#define pii pair<int, int>
const long long inf=2147483647;
const int maxn=100010;
const int maxm=500010;
using namespace std;
int n,m,s,tot=0;
int dis[maxn],head[maxm];
bool vis[maxn];
struct Edge
{
  int next,to,dis;
}edge[maxm];

void addedge(int from,int to,int dis) 
{ 
  edge[++tot].next=head[from]; 
  edge[tot].to=to; 
  edge[tot].dis=dis; 
  head[from]=tot;
}

void dij()
{
  for(int i=1; i<=n; i++) 
  {
    dis[i]=inf; 
   	vis[i]=false; 
  }
  // first存储距离,second存储节点编号
	priority_queue<pii,vector<pii>,greater<pii>> pq;
  pq.push({0,s}); 
  dis[s]=0; 
  while(!pq.empty())
  {
    auto u=pq.top(); 
    pq.pop();
    if(vis[u.second])
    	continue;
    vis[u.second]=true;
    for(int i=head[u.second]; i; i=edge[i].next) 
    {
      int v=edge[i].to;
      if(dis[v]>u.first+edge[i].dis)
      {
        dis[v]=u.first+edge[i].dis;
        pq.push({dis[v],v});
      }
    }
  }
}

int main()
{
	  scanf("%d%d%d",&n,&m,&s);
	  for(int i=1; i<=m; i++)
	  {
	    int u,v,w;
	    scanf("%d%d%d",&u,&v,&w);
	    addedge(u,v,w); 
	  }
	  dij(); 
	  printf("%d",dis[1]);
	  for(int i=2;i<=n;i++)
	      printf(" %d",dis[i]);
	  return 0;
} 

但是,dijkstra不能解决权值带有负数的问题
在这里插入图片描述
用dijkstra算法,会认为1到3的最短距离时1,但实际是0。

Bellman-Ford

bellman-ford算法进行 n-1 次更新(一次更新是指用所有节点进行一次松弛操作)来找到到所有节点的单源最短路。

  • 初始化
  • 迭代n-1次,每次遍历所有边
  • 对于每一条边(a到b,权值为w) 松弛三角不等式: dis[b]=min(dis[b],dist[a]+w);

bellman-ford算法和dijkstra其实有点相似,该算法能够保证每更新一次都能确定一个节点的最短路,但与dijkstra不同的是,并不知道是哪个节点的最短路被确定了,只是知道比上次多确定一个,这样进行n-1次更新后所有节点的最短路就都确定了。

那么为什么每次更新都能多找到一个能确定最短路的节点:

  1. 将所有节点分为两类:已确定最短距离的节点和剩余节点。
  2. 这两类节点满足这样的性质:已确定最短距离的节点的最短路值都比剩余节点的最短路值小。
  3. 有了上面两点说明,易知到剩余节点的路径一定会经过已确定节点。
  4. 而从已确定节点连到剩余节点的所有边中的最小的那个边,用这条边所更新后的剩余节点就一定是确定的最短距离,从而就多找到了一个能确定最短距离的节点,不用知道它到底是哪个节点。

bellman-ford算法的一个优势是可以用来计算带有负权值的最短路径问题以及判断是否存在负环(一个环的权值之和为负数)问题。因为,如果不存在负环,进行了 n-1 次所有边的松弛操作后,每个节点的最短距离都确定了,再进行一次所有边的松弛操作时,不会改变结果。但是如果存在负环,那么绕负环一圈会减少路径的长度,那么最后更新一次就会改变结果。

例题:

计算带有负权值的最短路径问题
洛谷P3371 【模板】单源最短路径(弱化版)

#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 2e5 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int maxn = 1e4+5;
const int maxm = 5e5+5;
struct Edge
{
    int u,v,w;
}e[maxm];

ll dis[maxn];
int n,m,s;

int main()
{
//	freopen("in.txt", "r", stdin);
	qc;
    cin>>n>>m>>s;
    for(int i=0;i<m;i++)
    	cin>>e[i].u>>e[i].v>>e[i].w;
    for(int i=1;i<=n;i++)
    	dis[i]=2147483647;
    dis[s]=0;
    bool flag;
    for(int i=1;i<n;i++)    //Bellman-Ford板子:n-1次对所有边进行松弛操作
    {
        flag = true;
        for(int j=0;j<m;j++)    //遍历所有边
        {
            if(dis[e[j].u]+e[j].w<dis[e[j].v])     //松弛操作
            {
                dis[e[j].v]=dis[e[j].u]+e[j].w;
                flag = false;       //如果有更新,置flag为false
            }
        }
        if(flag) break; //如果一次遍历所有边都没有更新dis值,那显然之后遍历所有边也不会更新dis值了,直接跳出即可
    }
    printf("%lld",dis[1]);
    for(int i=2;i<=n;i++)
        printf(" %lld",dis[i]);
	return 0;
}

判断是否存在负环问题:
洛谷P3385 【模板】负环

#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 2e5 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int maxn = 2e3+5;
const int maxm = 3e3+5;
struct Edge
{
    int u,v,w;
}e[maxm<<1];
inline int add(int a,int b) //∞+x=∞,所以要手写加法函数判断这种情况
{
    if(a==inf || b==inf) return inf;
    else return a+b;
}

ll dis[maxn];
int n,m,s;

int main()
{
//	freopen("in.txt", "r", stdin);
	qc;
	int T; 
	cin>>T;
	while(T--)
	{
		memset(e,0,sizeof(e));
        memset(dis,inf,sizeof(dis));
        dis[1]=0;
		cin>>n>>m;
		int k=0,u,v,w;
	    for(int i=0;i<m;i++)
	    {
	    	cin>>u>>v>>w;
	    	e[k++]=(Edge){u,v,w};
	    	if(w>=0) e[k++]=(Edge){v,u,w};
		}
	    bool flag=false;
	    for(int i=1;i<n;i++)    //Bellman-Ford板子:n-1次对所有边进行松弛操作
	    {
	        flag = true;
	        for(int j=0;j<k;j++)    //遍历所有边
	        {
	            if(add(dis[e[j].u],e[j].w)<dis[e[j].v])     //松弛操作
	            {
	                dis[e[j].v]=add(dis[e[j].u],e[j].w);
	                flag = false;       //如果有更新,置flag为false
	            }
	        }
	        if(flag) break; //如果一次遍历所有边都没有更新dis值,那显然之后遍历所有边也不会更新dis值了,直接跳出即可
	    }
	    flag=false;
	    for(int j=0;j<k;j++)    //遍历所有边
	    {
	        if(add(dis[e[j].u],e[j].w)<dis[e[j].v])     //松弛操作
	        {
	            dis[e[j].v]=add(dis[e[j].u],e[j].w);
	            flag=true;
	            break;
	        }
	    }
	    if(flag)
	    	cout<<"YES"<<endl;
	    else
	    	cout<<"NO"<<endl;
	}
    
	return 0;
}

Spfa

spfa可以看成是bellman-ford的队列优化版本。bellman每一轮用所有边来进行松弛操作可以多确定一个点的最短路径,但是每次都把所有边拿来松弛就太浪费了,其实只有那些已经确定了最短路径的点所连出去的边做松弛操作才是有效的,所以我们要找哪些可能是已知节点的点,也就是之前松弛后更新的点,已知节点必然在这些点中。
spfa的做法就是把每次更新了的点放到队列中记录下来。

例题:

hdu-2544 最短路

#include <bits/stdc++.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#define ll long long
#define qc ios::sync_with_stdio(false); cin.tie(0);cout.tie(0)
using namespace std;
const int MAXN = 2e4 + 7;
const int inf = 2147483647;
const ll INF = 0x3f3f3f3f3f3f3f3f;

int n,m;      // 
ll dis[MAXN];        // 存储每个点到起点的最短距离
bool inq[MAXN];     // 存储每个点是否在队列中
vector<pair<int,ll> >e[MAXN];// 每条边的边集 

void init()
{
	for(int i=0;i<MAXN;i++)
		e[i].clear();
	for(int i=0;i<MAXN;i++)
		inq[i]=false;
	for(int i=0;i<MAXN;i++)
		dis[i]=inf;
}

void spfa(int s)
{
	int x,y;
	ll z;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%lld",&x,&y,&z);
		e[x].push_back(make_pair(y,z));
		e[y].push_back(make_pair(x,z));
	} 
    queue<int> q;
    q.push(s);
    inq[s] = true;
    dis[s]=0;
    while (!q.empty())
    {
        int now=q.front();
        q.pop();
        inq[now]=false;
        int len=e[now].size();
        for (int i=0;i<len;i++)
        {
            int j=e[now][i].first;
            if (dis[j]>dis[now]+e[now][i].second)
            {
                dis[j]=dis[now]+e[now][i].second;
                if(inq[j])
                	continue;
                q.push(j);
                inq[j]=true;
            }
        }
    }
}

int main()
{
	qc;
	while(scanf("%d%d",&n,&m))
	{
		if(n==0)
			break;
		init();
		spfa(1);
		printf("%lld\n",dis[n]);
	}
	return 0;
}

碎碎念:

感谢 大佬 博客对我的启发!

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值