HDU2544最短路的四种算法(dijkstra+Floyd+spfa+Bellmam_Ford)

虽然这道题不用那么大费周章的用四个算法,这里只是根据这个题目进行四个算法的学习总结,也是记录一下自己的学习过程,如果有讲错的地方大家可以指正。
原题连接:http://acm.hdu.edu.cn/showproblem.php?pid=2544
Description
在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的t-shirt。但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,却是非常累的!所以现在他们想要寻找最短的从商店到赛场的路线,你可以帮助他们吗?
Input
输入包括多组数据。每组数据第一行是两个整数N、M(N<=100,M<=10000),N表示成都的大街上有几个路口,标号为1的路口是商店所在地,标号为N的路口是赛场所在地,M则表示在成都有几条路。N=M=0表示输入结束。接下来M行,每行包括3个整数A,B,C(1<=A,B<=N,1<=C<=1000),表示在路口A与路口B之间有一条路,我们的工作人员需要C分钟的时间走过这条路。
输入保证至少存在1条商店到赛场的路线。
Output
对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间
Sample Input 1
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
Sample Output 1
3
2

Dijkstra算法:

这是一道关于图的算法题目,dijkstra(迪杰斯特拉)算法,也叫最短路径算法,这个算法主页用到广度搜索原来,也用了贪心算法在里面。

这里介绍一下dijkstra的思路,主页用的思想是广度优先搜索算法和贪心算法,首先从源点出发,第一步是找源点的相邻节点,看可以访问哪一节点,更新相邻节点的值(如果从这个这个节点到相邻节点的值小于相邻节点的值,就更新),接着用贪心思想去最小的节点,然后再从该节点去找相邻节点,然后更新节点的值,一但相邻节点的值小于之前的值,则更新此节点的值,一直循环循环,直到遍历完所有节点。

Dijkstra步骤:
1.首先更新源节点相邻节点的值
2.从中找到相邻节点的最小值,此节点的最小值已经确定
3.进入此节点,找此节点所有相邻节点,回到2步骤
4.当所有节点都加入最小值集合,遍历结束

算法核心:
类是图的深搜宽搜,先建立访问控制数组v[maxn],存储图的数组cost[maxn],
最大的区别就是要多建立一个存储最低值的数组lowcost[maxn][maxn].
核心代码:

for(int i=0;i<n;i++){
		int k=-1;
		int min=INF;
		for(int j=0;j<n;j++){//找出当前还未访问且最小的节点 
			if(!v[j]&&lowcost[j]<min){
				min=lowcost[j];
				k=j;
			}
		}
		v[k]=true;//k节点访问过 
		for(int j=0;j<n;j++){//开始访问与k相连的其他节点 
			if(!v[j]&&lowcost[k]+cost[k][j]<lowcost[j]){
				lowcost[j]=lowcost[k]+cost[k][j];
			}
		}
	}

第二个for循环是步骤中的2,一直在寻找最小节点,
第三个for是开始遍历当前最小节点的所有相邻节点,更新当前各节点的值。

AC代码:

#include<iostream>
#include<string.h>
using namespace std;
const int MAXN=1010;
const int INF=0x3f3f3f3f; 
bool v[MAXN];//访问数组 
int lowcost[MAXN]; //保存最小的访问值
int cost[MAXN][MAXN];

void dijkstra(int n){
	for(int i=0;i<n;i++){//初始化lowcoat,v,pre数组 
		lowcost[i]=INF;
		v[i]=false;
	}
	for(int i=0;i<n;i++){
		lowcost[i]=cost[0][i];
	}
	v[0]=true;
	lowcost[0]=0;
	for(int i=0;i<n;i++){
		int k=-1;
		int min=INF;
		for(int j=0;j<n;j++){//找出当前还未访问且最小的节点 
			if(!v[j]&&lowcost[j]<min){
				min=lowcost[j];
				k=j;
			}
		}
		v[k]=true;//k节点访问过 
		for(int j=0;j<n;j++){//开始访问与k相连的其他节点 
			if(!v[j]&&lowcost[k]+cost[k][j]<lowcost[j]){
				lowcost[j]=lowcost[k]+cost[k][j];
			}
		}
	} 
} 
int main(){
	int n,m;
	while(cin>>n>>m){
		if(n==0&&m==0)
			break;
		int beg,end,value;
		memset(cost,INF,sizeof(cost));
		for(int i=0;i<m;i++){
			cin>>beg>>end>>value;
			cost[beg-1][end-1]=cost[end-1][beg-1]=value;
		}
		dijkstra(n);
		cout<<lowcost[n-1]<<endl;
	}
	return 0;
}

Floyd算法:

Floyd算法属于动态规划,Dijstra算法属于贪心算法

Floyd的原理就是在ij点之间加入k点,看a[i][j]是否大于a[i][k]+a[k][j],如果a[i][j]>a[i][k]+a[k][j]就更新a[i][j]=
a[i][k]+a[k][j]

核心算法:

for(int k=1;k<=n;k++){//k为插入ij之间的节点 
			for(int i=1;i<=n;i++){
				for(int j=1;j<=n;j++){
					if(a[i][j]>a[i][k]+a[k][j])//如果 a[i][j]>a[i][k]+a[k][j]
						a[i][j]=a[i][k]+a[k][j];
				}
			}
		}

AC代码:

#include<iostream>
#include<string.h>
using namespace std;
#define INF 0x3f3f3f3f
int a[100][100];
int main(){
	int n,m;
	while(cin>>n>>m){
		if(n==0&&m==0)
			break;
		memset(a,INF,sizeof(a));
		for(int i=0;i<n;i++)
			a[i][i]=0;
		int x1,x2,c;
		for(int i=0;i<m;i++){
			cin>>x1>>x2>>c;
			a[x1][x2]=c;
			a[x2][x1]=c;
		}
		for(int k=1;k<=n;k++){//k为插入ij之间的节点 
			for(int i=1;i<=n;i++){
				for(int j=1;j<=n;j++){
					if(a[i][j]>a[i][k]+a[k][j])//如果 a[i][j]>a[i][k]+a[k][j]
						a[i][j]=a[i][k]+a[k][j];
				}
			}
		}
		cout<<a[1][n]<<endl;
	}
	return 0;
}

Spfa(Shortest Path Faster Algorithm):

顾名思义,最短路快速算法,这是西安交大段凡丁1994年发表的,但是不知道为什么好像没有被公认。
先说一下前面的dijkstra:是贪心算法
Floyd:是动态规划算法
这个spfa:是动态逼近法

这里补充一个名词,松弛:松弛操作的原理是著名的定理:“三角形两边之和大于第三边”,在信息学中我们叫它三角不等式,所谓的松弛,就是判定是否dis[j]>dis[i]+w[I,j],如果该式成龙则讲dis[j]减小到dis[i]+w[I,j]
这段话来自博客:http://www.pianshen.com/article/7830107886/

那么spfa是怎么动态逼近呢,借助一下Floyd算法,那么Floyd算法是不停的借助中间节点去变小,那么这里也差不多但是稍微有点不一样,spfa算法变小之后,觉得这个变小的可能会引起其他的变小,那么我们又遍历这个变小的节点,这就是动态逼近,一旦变小,就走这里,慢慢的逼近最小值。

怎么实现呢,网上的代码都借助了队列辅助,就是如果某个点的值变小,就把这个点加入队列,去寻找有没有引起其他点变小,我们也叫这个队列叫维护队列。

另外在时间效率上也很可观,迪杰斯特拉算法在访问的时候是for从头到尾找可以变小的节点,但是spfa新建了临接矩阵去存储边,打个比较极限的比方,假设迪杰斯特拉算法去遍历一个环,这个环有200个节点,然后每个节点只喝相邻的两个节点相邻,那么如下图
在这里插入图片描述
迪杰斯特拉算法的话for算法20次,20次里面也19次,那么才19条边,这个for循环效率19/400,这个算法太低了,假设再大一点1000,那么这个算法肯定会超时。
在spfa算法中采用了邻接矩阵方法存储边,这个点有多少条边,就有访问多少次,这样效率达到了1:1

邻接矩阵:

for(int i=0;i<m;i++){
			cin>>x>>y>>w;
			if(a[x][y]!=0&&w>a[x][y])
				continue;
			b[x][0]++;	b[x][b[x][0]]=y;	a[x][y]=w;
			b[y][0]++;	b[y][b[y][0]]=x;	a[y][x]=w;
	}
b[x][0]存储x有多少条边,b[x][b[x][0]]存储这到y的节点小标
a[x][y]存储边的权

spfa步骤:
1.把起始节点加入队列
2.出队,找出变小的相邻节点,如果该节点不在队列,加入队列
3.重复循环2,直到队列为空

核心代码(spfa):

void spfa(int s){
	for(int i=0;i<=n;i++)//初始化保存最小数组 
		dis[i]=INF;
	dis[s]=0;//起始节点距离为0 
	v[s]=1;
	queue<int> q;//新建维护数组 
	int now;
	q.push(s);
	while(!q.empty()){//若果队列不为空,继续循环 
		now=q.front();//取出最先元素 
		q.pop();
		//cout<<"节点"<<now<<endl; 
		v[now]=0;//出队恢复访问位 
		for(int i=1;i<=b[now][0];i++){//遍历now的所有边 
			if(dis[b[now][i]]>dis[now]+a[now][b[now][i]]){//如果i这个点,通过now节点可以使dis变小,那么则把i加入队列 
				dis[b[now][i]]=dis[now]+a[now][b[now][i]];
				//cout<<now<<" "<<b[now][i]<<" "<<dis[b[now][i]]<<endl;
				if(v[b[now][i]]==0){
					q.push(b[now][i]);//把i加入队列 
					v[b[now][i]]=1;
				} 
			}
		}
	}
	return ;
}

可能觉得这个算法个广度搜索很像,确实很像,就是吧这个点的所有相邻节点介入队列,但是与广度搜索不一样的是,广度出队的节点就不可能再入队,而spfa可能再入队(v[now]=0;)。

AC代码:

#include<iostream>
#include<string.h>
#include<queue>
using namespace std;
#define INF 0x3f3f3f3f
int n,m; 
int dis[5000],v[5000],a[5000][5000],b[5000][5000];
void spfa(int s){
	for(int i=0;i<=n;i++)//初始化保存最小数组 
		dis[i]=INF;
	dis[s]=0;//起始节点距离为0 
	v[s]=1;
	queue<int> q;//新建维护数组 
	int now;
	q.push(s);
	while(!q.empty()){//若果队列不为空,继续循环 
		now=q.front();//取出最先元素 
		q.pop();
		//cout<<"节点"<<now<<endl; 
		v[now]=0;//出队恢复访问位 
		for(int i=1;i<=b[now][0];i++){//遍历now的所有边 
			if(dis[b[now][i]]>dis[now]+a[now][b[now][i]]){//如果i这个点,通过now节点可以使dis变小,那么则把i加入队列 
				dis[b[now][i]]=dis[now]+a[now][b[now][i]];
				//cout<<now<<" "<<b[now][i]<<" "<<dis[b[now][i]]<<endl;
				if(v[b[now][i]]==0){
					q.push(b[now][i]);//把i加入队列 
					v[b[now][i]]=1;
				} 
			}
		}
	}
	return ;
}
int main(){
	int x,y,w,s,e;
	while(cin>>n>>m){
		if(n==0&&m==0)
			break;
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
		for(int i=0;i<m;i++){
			cin>>x>>y>>w;
			if(a[x][y]!=0&&w>a[x][y])
				continue;
			b[x][0]++;	b[x][b[x][0]]=y;	a[x][y]=w;
			b[y][0]++;	b[y][b[y][0]]=x;	a[y][x]=w;
		}
		spfa(1);
		cout<<dis[n]<<endl;
	}
	return 0;
}

Bellmam-Ford算法:

这个算法是美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。
在前面,介绍的三个算法都可以求最短路,但是遇到负环问题就都无法解决,这里就有另外有个Bellmam-Ford算法去解决问题,那这个算法大概思路是怎么样的呢?
网上的博客都分为三步:

1.初始化最小路径数组,出来起始点之外,都置为无穷大(建议用#define INF 0x3f3f3f3f),
2.分内外循环,外循环是n个点最多通过n-1次松弛就可以达到最佳效果(这也是最难理解的),内循环是对每边加入最小边数组比较,如果最小边大于加入的边额更新(跟其他算法一样),那么这样一来,算法的时间效率就是o(v*e)v是点,e是边
3.再一次遍历所有边,是不是存在有最小边的值还在变小,如果有变小就存在负环。

第二点需要结合图解释
在这里插入图片描述
像这么一个图,如果从左边开始遍历到右边的话,先进行初始化(假设所有边都为1)
在这里插入图片描述
第一次外循环,执行完整一次内循环
在这里插入图片描述
因为只有d[0]+e[1]<d1,其他都是INF+e[n]>INF,无法更新

第二次外循环,执行完整一次内循环
在这里插入图片描述
因为只有d[2]+e[2]<d2,除了d[0]+e[1]=d[1]置为,其他都是INF+e[n]>INF,无法更新



以此类推,最终,没有回路图最多通过n-1次松弛就可以得到所以最短路径
在这里插入图片描述

没有回路的图最多通过n-1松弛就可以得到最短路径,假设有回路图的话
在这里插入图片描述
很明显不用n-1次,松弛的次数小于n-1次,但是输入的图不知道有没有回路,所以都松弛n-1次就可得到最短路了。

核心代码:

for(int i=1;i<n;i++){
		for(int j=0;j<m;j++){
			//cout<<lowcost[edge[j].u]<<" "<<lowcost[edge[j].v]+edge[j].cost<<endl;
			if(lowcost[edge[j].u]>lowcost[edge[j].v]+edge[j].cost)//如果是单源最短路的话,就只有一个IF 
				lowcost[edge[j].u]=lowcost[edge[j].v]+edge[j].cost;
			if(lowcost[edge[j].v]>lowcost[edge[j].u]+edge[j].cost)
				lowcost[edge[j].v]=lowcost[edge[j].u]+edge[j].cost;
		}
	}
	bool flag=1;
	for(int j=0;j<m;j++){//经过n-1次松弛了,还进行一次松弛,如果出现变小路径的话,则存在负环 
		if(lowcost[edge[j].u]>lowcost[edge[j].v]+edge[j].cost){
			return flag=0;
			break;
		}
		if(lowcost[edge[j].v]>lowcost[edge[j].u]+edge[j].cost){
			return flag=0;
			break;
		}	
	}第一个两层for是进行n-1次松弛
第二个for是判断是否存在最小边

AC代码:

#include<iostream>
#include<string.h>
using namespace std;
#define INF 0x3f3f3f3f
struct edge{
	int v,u,cost;
}edge[10001];
int lowcost[101];
int n,m;
bool Bellman_Ford(int x){
	memset(lowcost,INF,sizeof(lowcost));
	lowcost[x]=0;
	for(int i=1;i<n;i++){
		for(int j=0;j<m;j++){
			//cout<<lowcost[edge[j].u]<<" "<<lowcost[edge[j].v]+edge[j].cost<<endl;
			if(lowcost[edge[j].u]>lowcost[edge[j].v]+edge[j].cost)//如果是单源最短路的话,就只有一个IF 
				lowcost[edge[j].u]=lowcost[edge[j].v]+edge[j].cost;
			if(lowcost[edge[j].v]>lowcost[edge[j].u]+edge[j].cost)
				lowcost[edge[j].v]=lowcost[edge[j].u]+edge[j].cost;
		}
	}
	bool flag=1;
	for(int j=0;j<m;j++){//经过n-1次松弛了,还进行一次松弛,如果出现变小路径的话,则存在负环 
		if(lowcost[edge[j].u]>lowcost[edge[j].v]+edge[j].cost){
			return flag=0;
			break;
		}
		if(lowcost[edge[j].v]>lowcost[edge[j].u]+edge[j].cost){
			return flag=0;
			break;
		}	
	}
	return flag;
}
int main(){
	while(cin>>n>>m){
		if(n==0&&m==0)
			break;
		for(int i=0;i<m;i++){
			cin>>edge[i].v>>edge[i].u>>edge[i].cost;
		}	
		if(Bellman_Ford(1)){
			cout<<lowcost[n]<<endl;
		}
	}
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值