最短路算法(Floyd、Dijsktra、Bellman-Ford、SPFA)

最短路算法基本可以分为以下两个步骤:

①初始化(任意两边的距离)

②松弛操作

在图论中,最关键的是如何建图。

在最短路算法中,首先要处理数据,在这个时候,要考虑该用那种方式建图。

比较常见的建图方式:邻接链表、邻接矩阵、前向星、链式前向星、十字链表。

对于这五种建图方式,在这里不做详细讨论,只是大概介绍一下优点和缺点。

邻接链表:适合点多的图

邻接矩阵:适合边多的图

链式前向星:适合不带重边的图。除此之外,无论点多还是边多,链式前向星都能表现出很完美的效率。

前向星和十字链表个人用的很少,不做描述。


①Floyd最短路算法

Floyd最短路算法的代码很短,5行就能搞定,但是思想却非常值得学习。

采用动态规划思想:

dp[k][i][j]表示从i到j之间可以经过1~k节点的最短路径。

状态转移方程:dp[k][i][j]=min{dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]};

对于dp[k][i][j],可以从dp[k-1][i][j]不经过k结点,或者从dp[k-1][i][j]经过k节点,即dp[k-1][i][k]+dp[k-1][k][j];

因为dp[k]只和dp[k-1]有关,所以可以省略dp最外层的一维空间。

(在初始化操作中,需要将dp初始化为无穷大,但是在松弛操作中,又需要避免数据溢出,所以需要选择一个合理的“无穷大”,0x3f3f3f3f是1061109567)

int dp[maxn][maxn];

void Floyd(){
	memset(dp,0x3f3f3f3f,sizeof(dp));
	for(int k=1;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
			}
		}
	}
}

下面介绍一下如何打印路径。

用path[i][j]表示j节点的前驱。在更新dp[i][j]最短路径的同时记录更新path[i][j];

(推荐题:hdu1385)

void print(int a,int b){  
    printf("Path: %d",a);  
    int next=path[a][b];  
    while(next!=b){  
        printf("-->%d",next);  
        next=path[next][b];  
    }  
    if(a!=b)  
        printf("-->%d",b);  
    putchar(10);  
} 


②Dijsltra最短路算法

Dijsktra算法是求单源路径最短路问题。

在n^2的效率下计算出源点src到任一点的最短路径,而Floyd算法是在n^3的效率下计算出任意两点的最短距离。

另外一点,在最短路中也需要注意考虑重边的情况,养成好习惯。

下面先给出Dijsktra算法的详细代码并标注解释:

const int INF=0x3f3f3f3f;
const int maxn=1010;
int map[maxn][maxn];//map[i][j]表示从i到j的距离
int dis[maxn];//表示从源点到i点的最短距离
bool vis[maxn];//记录该点是否已经访问过

int Dijsktra(int src,int des){
	memset(dis,INF,sizeof(dis));//初始化dis数组
	dis[src]=0;//源点到本身的距离为0
	vis[src]=true;//标记源点
	for(int i=1;i<=n-1;i++){//只需要更新n-1次
		int pos,min=-INF;
		for(int j=1;j<=n;j++){
			if(!vis[j]&&dis[j]<min)
				min=dis[pos=j];
		}
		if(min==INF) return -1;//如果不存在,返回-1
		vis[pos]=1;
		for(int j=1;j<=n;j++){//更新dis数组
			if(!vis[j]&&dis[j]>dis[pos]+map[pos][j])
				dis[j]=dis[pos]+map[pos][j];
		}
	}
	return dis[des];//返回源点到目的点的最短距离
}

应该可以注意到,通俗一点来讲,Dijsktra就是彼此找出一个距离src最近的点pos,以这个点作为中介点更新src到其他所有点的最短距离。

在更新的过程中,已访问的节点做一个标记,这样可以提高效率,源点初始化后不需要再访问,所以更新n-1次即可。

但是在Dijsktra的优点在于,在查找中介点的过程中,需要遍历所有点,效率为o(n),但是如果用二叉堆来优化的话,效率只需要o(logn)。

这里,我们用priority_queue来实现。

如果不用优先队列的话,Dijsktra的效率为O(2|E|+|v|^2),用优先队列查找里源点最近的点时效率为O(log|V|),整体效率为O(2|E|+|v|log|V|)

具体代码如下(Dijsktra+优先队列这种方式也是有必要掌握的。):

struct edge{
	int to,cost;
	edge(){}
	edge(int to,int cost):to(to),cost(cost){}
};

typedef pair<int,int> P;
vector<edge> G[maxn];
int dis[maxn][maxn];//dis[i][j]表示i->j的最短距离
int a,b,c;

void Dijsktra(int s){
	priority_queue<P, vector<P>, greater<P> > q;//优先队列优化,维护最短路径
	memset(dis,INF,sizeof(dis));
	dis[s][s]=0;;
	q.push(P(0,s));
	while(!q.empty()){
		P p=q.top();q.pop();
		int v=p.second;
		if(dis[s][v]<p.first) continue;
		for(int i=0;i<G[v].size();i++){//更新最短路径
			edge e=G[v][i];
			if(dis[s][e.to]>dis[s][v]+e.cost){
				dis[s][e.to]=dis[s][v]+e.cost;
				q.push(P(dis[s][e.to],e.to));
			}
		}
	}
}


 

Bellman-Ford算法:

Bellman-Ford算法可以解决帶负环的问题。这也是它相对于上面两个算法最大的优势所在。

对每一条边e[x],如果dis[edge[x].u]>dis[edge[x].v]+edge[x].w,则edge[x].u=edge[x].v+edge[x].w;该操作至多只需要进行n-1次

为了判断图中是否存在负环,即权值之和<0的环路,对于每一条边e[x],如果存在dis[e[x].u]>dis[edge[x].v]+edge[x].w,则图中存在负环,无法求出单源最短路径。

(推荐提:POJ 1860)

const int INF=0x3f3f3f3f;
const int maxn=1010;
int dis[maxn];
int e;

void init(){
	memset(dis,INF,sizeof(dis));
	e=0;
}

struct node{
	int u;
	int v;
	int w;
}edge[maxn];

void addEdge(int u,int v,int w){
	edge[e].u=u,edge[e].v=v,edge[e].w=w;
	e++;
}

void relax(int x){//松弛操作
	if(dis[edge[x].u]>dis[edge[x].v]+edge[x].w){
		edge[x].u=edge[x].v+edge[x].w;
	}
}

bool Bellman_Ford(int src){
	dis[src]=0;
	for(int i=1;i<=n;i++){
		for(int j=0;j<e;j++){
			relax(j);//对每一条变进行松弛操作
		}
	}
	for(int i=0;i<e;i++){
		if(dis[edge[i].u]>dis[edge[i].v]+edge[i].w){
			return false;//有回路
		}
	}
	return true;//无回路
}

④SPFA最短路算法

SPFA其实是Bellman-Ford算法的队列优化。

先取队首元素u,并将其出队,取消标记,将于点u直接相连的所有点进行松弛操作,如果能进行松弛,那么就更新dis数组。

更新结束后,判断该点是否在队列中,如果不在,那么将点入列,然后进行标记。

判断有无负环:如果某个点进入队列的次数超过n次,则存在负环。

下面给出用STL队列实现的算法:

(推荐题:POJ 3259)

const int INF=0x3f3f3f3f;
const int maxn=1010;
int dis[maxn],head[maxn],inQueue[maxn];
bool vis[maxn];
int e;

void init(){
	memset(dis,INF,sizeof(dis));
	memset(head,-1,sizeof(head));
	memset(vis,0,sizeof(vis));
	memset(inQueue,0,sizeof(inQueue));
	e=0;
}

struct node{//链式前向星建图
	int v;
	int w;
	int next;
}edge[maxn];

void addEdge(int u,int v,int w){
	edge[e].v=v,edge[e].w=w,edge[e].next=head[u],head[u]=e;
	e++;
}

bool Spfa(int src){
	dis[src]=0;
	vis[src]=1;
	queue<int>Q;
	Q.push(src);//源点放入队列
	while(!Q.empty()){
		int s=Q.front();
		Q.pop();
		vis[s]=0;//出队时取消标记
		inQueue[s]++;
		if(inQueue[s]>n) return false;//如果一个点入队n次,表明存在负环
		for(int i=head[s];i!=-1;i=edge[s].next){
			if(dis[edge[i].v]>dis[s]+edge[i].w){//松弛操作
				dis[edge[i].v]=dis[s]+edge[i].w;
				if(!vis[edge[i].v]){
					vis[edge[i].v]=1;//入队时进行标记
					Q.push(edge[i].v);
				}
			}
		}
	}
	return true;
}
如果想要快速判断是否存在负环,Dfs深搜的效率会明显较高。


在无负环的情况下,选择Dijsktra最短路算法效率会比较高,SPFA算法的时间不稳定,Bellman-Ford和Floyd算法的效率都比较高。

在有负环的情况下,选择SPFA算法会比较合理一些。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值