「关于单源最短路径的三种算法的思考(不包括计算多点路径的Floyd算法)」

6 篇文章 0 订阅

Bellman-Ford算法(可以处理负值,检测负权回路,但复杂度过高)开始,

Bellman-ford算法是求含负权图的单源最短路径算法;

效率很低,但代码很容易写;

复杂度很高:O(VE) v为顶点数,e为边数。

思想:进行不停地松弛,每次松弛把每条边都更新一下,直到一次遍历所有边的松弛操作无法更新结点间的最短距离。

n-1次松弛后还能更新,则说明图中有负环因为最短路不会同时经过同一个点两次,也就是说最多通过(n-1)条边),无法得出结果,否则就成功完成。

算法描述:

对每条边进行|V|-1松弛操作;

如果存在(u,v)E使得dis[u]+w<dis[v],则存在负权回路;否则dis[v]即为sv的最短距离,pre[v]为前驱。

void relax(int u, int v, int weight){ // 松弛计算
    if(dist[v] > dist[u] + weight)
        dist[v] = dist[u] + weight;}
bool Bellman_Ford(){
    for(int i=1; i<=nodenum-1; ++i)
        for(int j=1; j<=edgenum; ++j)
            relax(edge[j].u, edge[j].v, edge[j].weight);
    bool flag = 1;
    for(int i=1; i<=edgenum; ++i) // 判断是否有负环路
        if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight){    //如果在nodenum-1次循环后还是可以进行松弛操作,则说明有负权回路
            flag = 0;    break;
        } return flag;
}

Dijkstra则是对Bellman-Ford算法的一种优化,是一种解决单源最短路径的贪心算法。

解决的问题:

    带权重的有向图上单源最短路径问题。且权重都为非负值同时适用于无向图和有向图。如果采用的实现方法合适,Dijkstra运行时间要低于Bellman-Ford算法和Spfa,因为竞赛中可能会造卡SPFA的数据,故我们通常采用Dijkstra算法。

其核心为:1.找到最短距离已经确定的点,并将其放入一个集合中,从它出发更新相邻顶点之间的距离。

                    2.此后不需再关心已经被放入集合中的点,当所有点都被放入集合中时,源点到所有点的最短距离则已被得出。

基本思想首先设置一个集合S;用数组dist[]来记录vS中各点的目前最短路径长度。然后不断地用贪心选择来扩充这个集合,并同时记录或修订数组dist[];直至S包含所有V中顶点。

解题过程图示:https://61mon.com/index.php/archives/194/comment-page-2#comment-509 

其复杂度为O(N^2),如果用堆优化则可达到O(|E|log|V|)。

此外,Dijkstra还可以很方便的给出最短路径的具体表示(代码下次完善时再写)

下面给出代码:

void dijkstra(int start)//从start点开始
{
    int i,j,k;
    memset(vis,0,sizeof(vis));//标记是否访问过
    for(i=1; i<=n; i++)//n为总点数
    {
        if(i==start)
            dis[i]=0;
        else
            dis[i]=INF;
    }
    for(i=1; i<=n; i++)
    {
        int r;
        int min=INF;
        for(j=1; j<=n; j++)
            if(!vis[j]&&dis[j]<min)
            {
                min=dis[j];
                r=j;
            }
        vis[r]=1;
        for(k=1; k<=n; k++)//对所有从r出发的边进行松弛
            if(dis[k]<(dis[r]+g[r][k]))
                dis[k]=dis[k];
            else
                dis[k]=dis[r]+g[r][k];
    }
    return;
}

使用优先队列优化代码:

struct edge{int to,cost;};
typedef pair<int,int>P;
int v;
vector<edge>G[MAX_V];
int d[MAX_V];
void dijkstra(int s)
{
    priority_queue<P,vector<P>,greater<P> >que; //通过指定greater<P>参数,堆按照从小到大的顺序取出值。
    /*当所有边的权值相同时,这种情况下,Dijkstra算法使用优先队列与普通队列的的效果是相同的,但算法复杂的会发生变化。需要注意。*/
    fill(d,d+V,INF);
    d[s]=0;
    que.push(P(0,s));
    while(!que.empty())
    {
        P p=que.top();
        que.pop();
        int v=p.second;
        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));
            }
        }
    }
}
#define _CRT_SECURE_NO_WARNINGS
/*
7 10
0 1 5
0 2 2
1 2 4
1 3 2
2 3 6
2 4 10
3 5 1
4 5 3
4 6 5
5 6 9
6
*/
#include <iostream>
#include <vector>
#include <utility>
#include <queue>
#include <functional>
#include <algorithm>
#include <cstdio>
using namespace std;

const int maxn = 1010 + 20;
const int INF = 99999999;
struct edge
{
    int to, cost;
};
typedef pair<int, int> P;      //first是最短距离,second是顶点的编号
int V, E;
vector<edge> G[maxn];
int d[maxn];
//void init();
void input();

void dijkstra(int s)
{
    //通过指定greater<P>参数,堆按照first从小到大的顺序取出值
    priority_queue<P, vector<P>, greater<P> > que;
    fill(d, d + V, INF);
    d[s] = 0;
    que.push(P(0, s));

    while (!que.empty()) {
        P p = que.top(); que.pop();
        int v = p.second;
        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));
            }
        }
    }
}

//void init()
//{    
//}

void input()
{
    int s, t, ct;
    edge tmp;
    for (int i = 0; i < E; i++) {
        cin >> s >> t >> ct;
        tmp.to = t; tmp.cost = ct;
        G[s].push_back(tmp);
        //无向图,反向也需要连接
        tmp.to = s; tmp.cost = ct;
        G[t].push_back(tmp);
    }
}

int main()
{
    cin >> V >> E;
    //init();
    input();
    dijkstra(0);
    int ov;
    cin >> ov;
    cout << d[ov] << endl;
    return 0;
}


SPFA算法(可以用来处理负权路径,并检测负权回路)

主要思想:动态逼近法,用一个队列来保存所有待优化的结点,并且每次取出队列首位的点,将其邻接的所有点进行松弛操作,如果所操作的点的最短路径有所调整,且该点不在队中,则该被操作的点入队。直到该队列为空时,则该算法结束。

这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

算法复杂度:O(ke) (e为边数)最坏情况下复杂度为 O(n^2)

SPFA 在形式上和宽度优先搜索非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。设一个点用来作为迭代点对其它点进行改进的平均次数为k,有办法证明对于通常的情况,k在2左右。

实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空


上代码:(记不得是哪道题的了。。。但是可以拿来做spfa的模版。。。)

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
using namespace std;
const int mod = (int) 1e9+7;

int n,m,dis[205][205],dist[205];
bool inq[205]; //inq[i]代表i是不是在队列中 
void spfa(){
	queue<int> q;
	dist[1]=0;
	q.push(1);
	inq[1]=1;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		inq[u]=0; //u点已经离开队列,就把inq[u]置为0 
		for(int i=0;i<n;i++){
			if(dist[i]-dis[u][i]>dist[u]){
				dist[i]=dist[u]+dis[u][i]; //更新i点到起点的距离 
				if(inq[i])continue; //如果已经在队列里,跳过后续步骤 
				q.push(i); //把i点放入队列 
				inq[i]=1; //更新inq[i] 
			}
		}
	}
}

int main()
{
	while(~scanf("%d%d",&n,&m)){
		if(n==0&m==0)
		break;
		memset(dis,125,sizeof(dis));                //对所有点的最短路径的初始化,很重要
		memset(dist,125,sizeof(dist));             //输入边之前的各点距离的初始化,很重要
		for(int i=0;i<n;i++)dis[i][i]=0;
		int x,y,z;
		for(int i=0;i<m;i++){
			scanf("%d%d%d",&x,&y,&z);
			dis[x][y]=dis[y][x]=min(dis[x][y],z);         //此题为双向图,且两点之间可能不止一条双向的路径。
		}
		spfa();
		printf("%d\n",dist[n]);
	}
	return 0;
}
再来说一下spfa检测负权回路。。。因为每一个点进入队列的次数最多为n-1次(因为最短路不会同时经过同一个点两次,也就是说最多通过(n-1)条边,故对同一个点进入队列不会超过n-1次),故 

若一个点入队次数超过n,则有负权环

最后说一下最短路径本身怎么输出
    在一个图中,我们仅仅知道结点A到结点E的最短路径长度,有时候意义不大。这个图如果是地图的模型的话,在算出最短路径长度后,我们总要说明“怎么走”才算真正解决了问题。如何在计算过程中记录下来最短路径是怎么走的,并在最后将它输出呢?
    我们定义一个path[]数组,path[i]表示源点s到i的最短路程中,结点i之前的结点的编号(父结点),我们在借助结点u对结点v松弛的同时,标记下path[v]=u,记录的工作就完成了。
    如何输出呢?我们记录的是每个点前面的点是什么,输出却要从最前面到后面输出,这很好办,递归就可以了: 

c++ code:
void printpath(int k){
	if (path[k]!=0) printpath(path[k]);
	cout << k << ' ';
}


Floyd没什么好说的了。。。查找多点间的最短路径。。。复杂度O(N^3)
for (k=1;k<n;k++)
      for (i=1;i<n;i++)
      for (j=1;j<n;j++)
          if (d[i][j]>d[i][k]+d[k][j]) 
             d[i][j]=d[i][k]+d[k][j];                            //可以在此if语句中加一个path[]数组用以存储路径,简单,在此不赘述。
求最长路:将Bbellman-Ford或者SPFA算法中边的权值改为相反值即为求最长路。得到结果后取相反值即可。(不能用dijkstra)



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值