第十七次总结

补上之前欠了很久的总结

这次主要是总结最短路径问题,Flody,Dijkstra和Bellman-Ford的基础代码实现与思路

注:代码来源于《啊哈算法》

1.Floyd

用于求任意两个地方之间的最短路程

核心代码

for(int  k=1;k<=n;k++){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(arr[i][j]>arr[i][k]+arr[k][j])
			arr[i][j]=arr[i][k]+arr[k][j];
		}
	}
}

思路:假设我们要从1号顶点到5号顶点,我们就去比较直接从1->5的距离与1->n的距离+n->5之间的距离,如果否则较小,则说明我们找到了一个"桥",可以缩短直接从1->5之间的距离,经过枚举每一个顶到到每一个其他顶点的情况,来得到我们最终的所有路之间的最短路径

#include<bits/stdc++.h>
using namespace std;
int main(){
	int e[10][10],n,m,inf=999999999,t1,t2,t3;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			if(i==j)
			e[i][j]=0;
			else
			e[i][j]=inf;
		}
	for(int i=1;i<=m;i++){
		cin>>t1>>t2>>t3;
		e[t1][t2]=t3;
	}
	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				if(e[i][j]>e[k][j]+e[i][k])
				e[i][j]=e[i][k]+e[k][j];
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			printf("%10d",e[i][j]);
		}
		puts("");
	}
	return 0;	
}

2.Dijkstra

用于解决一个点到其他顶点之间的情况

下面我们以一号顶点到其他顶点的问题为例

 我们开始需要一个数组来存储一号顶点到其余各个顶点的初始路程,用于之后的比较

我们称这个dis数组为最短路程的“估计值”

(1)既然我们是求一号顶点到其他顶点的最短路径,那么我们就去找到距离一号顶点最近的顶点,即二号顶点,选择二号顶点后,dis[2]的"估计值",就变为了“确定值”,因为一号顶点到二号顶点的距离已经是最近的,

(2)我们选择二号顶点后,接下来看二号顶点可以走那些顶点,2->3,2->4,我们先讨论1->2->3的距离是否小于1->3,比较dis[3]dis[2]+e[2][3],显然,前者为12,后者为1+9=10,后者更小,我们称这种情况为“松弛”,此时便要更新dis数组的值,同样去比较1->2->41->4的值,更新后的值为

 (3)接下来在剩下的三号,四号,五号,六号,顶点中去寻找离一号顶点最近的顶点按照上面的方式去更新dis数组,也就是四号顶点,此时,dis[4]的值从“估计值”变为“确定值”,接下来寻找四号顶点所有出边4->3,4->5,4->6,用上面的方法进行松弛,更新完毕后的dis数组为

 4)继续在剩下的3,5,6号顶点中,选出离一号顶点最近的顶点,也就是3号顶点,进行松弛,然后最5号6号顶点进行松弛,得到最后的dis数组

 总体上就是目前dis数组中的值(1->x)与可以进行偏转的值(1->n+n->n)之间的比较。本质上思路与Floyd是一致的,都是去寻找两个点之间是否存在可以缩短距离的"桥"

#include<bits/stdc++.h>
using namespace std;
int main(){
	int e[100][100],dis[100],book[100];
	int inf=999999999,n,m,t1,t2,t3,min,tmep;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++){
		if(i==j)
		e[i][j]=0;
		else
		e[i][j]=inf;
	}
	for(int i=1;i<=m;i++){
		cin>>t1>>t2>>t3;
		e[t1][t2]=t3;
	}
	for(int i=1;i<=n;i++)
	dis[i]=e[1][i];
	 book[1]=1;
	 for(int i=1;i<=n-1;i++){
	 	min=inf;
	 	for(int j=1;j<=n;j++){
	 		if(book[j]==0&&dis[j]<min){
			min=dis[j];
			tmep=j;	
			 }
		 }
		 book[tmep]=1;
		 for(int v=1;v<=n;v++){
		 	if(e[tmep][v]<inf){
		 		if(dis[v]>dis[tmep]+e[tmep][v])
				  dis[v]=dis[tmep]+e[tmep][v];
			 }
		 } 
	 }
	 for(int i=1;i<=n;i++)
	 cout<<dis[i]<<" ";
}

3.Bellman-Ford

解决一个点到其他点之间最短路径寻找的问题,也可以解决负权回路问题,

负权回路概念

核心代码

for(int k=1;k<=n-1;k++)
	for(int i=1;i<=m;i++)
		if(dis[v[i]]>dis[u[i]]+w[i])
           dis[v[i]]=dis[u[i]]+w[i]

(1)对于下面这两行代码的解释

if(dis[v[i]]>dis[u[i]]+w[i])
dis[v[i]]=dis[u[i]]+w[i];

看看是否能通过u[i]->v[i](权值为w[i])这条边,使得1号顶点到v[i]号顶点的距离变短。

即1号顶点到u[i]号顶点的距离(dis[u[i]])加上u[i]->v[i]这条边的距离的和是否会比原先1号顶点直接到v[i]号顶点的距离小,也就是Dijkstr中的松弛,就是将边的信息存储到了几个数组中

(2)关于为什么是n-1次循环,我个人的看法

例子

 我们还是使用有一个dis数组来存储一号顶点到所有顶点的距离

 

每个顶点上方带下划线的数字就是该顶点的最短路的“估计值”,即dis数组对应的值,根据给出的顺序,我们先来处理2 3 2的情况(2--2->3),

  1. 即我们来判断dis[3]是否大于dis[2]+2;显然,二者都是无穷大,松弛失败
  2. 然后我们来判断第二条边,1 2 -3,显然,dis[2]>dis[1]+(-3),进而,dis[2]的值从无穷大变为-3,因此松弛完毕

 

经过这一次大的循环,dis[2]与dis[5]已经变小了,也就是一号顶点到二号顶点的距离已经变小了,一号顶点到五号顶点的距离也已经变小了

在次基础上,我们在进行一次循环,来进行松弛

 

第二次循环完后的结果与第一次循环完后的结果就差别很大了,我们可以这样理解:

第一次循环时没有桥,我们在松弛也就是在找桥,所以说,第二次循环时,两个点之后要是可以缩短距离,也就代表着它们之间存在一个桥,距离就变短,

第二次循环后,要是两个点的距离还可以变短,也就代表着,它们之间可以存在第二座桥

于是在第三次循环中,我们遇到这两个可以缩短距离的桥,就利用这个桥,缩短距离

故:需要循环几次,也就是去判断最多可以找到几个桥,两个点之间最多找到n-1个桥,也就是需要循环n-1次

核心代码的n-1次循环就是这样来的

#include<bits/stdc++.h>
using namespace std;
int inf=999999999;
int main(){
	int dis[100],n,m,u[100],v[100],w[100];
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	cin>>u[i]>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
	dis[i]=inf;
	dis[1]=0;
	for(int k=1;k<=n-1;k++)
		for(int i=1;i<=m;i++)
			if(dis[v[i]]>dis[u[i]]+w[i])
				dis[v[i]]=dis[u[i]]+w[i];
	for(int i=1;i<=n;i++)
	cout<<dis[i]<<" ";
}

Bellman-Ford我们还可以用来判断是否存在负权回图

根据执行完我们的核心代码之后,一号点到其他点的距离通过dis数组的缩短后,存在负的,说明该图是一个负权回图

#include<bits/stdc++.h>
using namespace std;
int inf=999999999;
int main(){
	int dis[100],n,m,u[100],v[100],w[100];
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	cin>>u[i]>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
	dis[i]=inf;
	dis[1]=0;
	for( int k=1;k<=n-1;k++){
		int check =0;
		for(int i=1;i<=m;i++){
			if(dis[v[i]]>dis[u[i]]+w[i]){
					dis[v[i]]=dis[u[i]]+w[i];
						check=1;//没有更新就跳出循环,节省时间 
			}
		}
		if(check==0)
				break;
	}
	int flat=0;
	for(int i=1;i<=n;i++)
	if(dis[v[i]]>dis[u[i]]+w[i])
	flat=1;
	if(flat==1){
		puts("此图存在负权回图");
			for(int i=1;i<=n;i++)
				cout<<dis[i]<<" ";
	}
	else{
			for(int i=1;i<=n;i++)
			cout<<dis[i]<<" ";
			puts("");
	puts("此图不存在负权回图");
	}
}

优化(SPFA):

在我们实行一次松弛操作后,就会因为有一些点已经求得其最短路的估计值就会一直保持不变,不再受后续松弛操作的影响,但是每次我们到还要去判断是否需要松弛,这里浪费了时间,故我们每次仅对最短路估计值发生了变化了的顶点的所有出边(相邻边)执行松弛操作。

如何知道那些点的最短路径发生了变化?我们使用一个队列来维护这些点。

例子

 我们使用dis来存放一号顶点到各个顶点的最短路径,初始时dis[1]=0,接下来将一号顶点入列,我们使用一个que数组以及两个tail和head两个变量来实现

最开始的情况

 

我们先对1 2 2 进行操作,发现dis[2]<dis[1]+2,进行松弛,并且二号顶点不在队列中,因此将二号顶点进行入队

 对一号顶点剩余的出边进行松弛后,处理完毕后的dis数组与que队列变为

 

处理完一号顶点后,对一号顶点进行出队,再最新队首二号顶点进行以上处理

但是我们要特别注意:在处理2 5 7 的时候,因为dis[5]<dis[2]+7,可以进行松弛操作,但是5号顶点已经到que队列中,于是不在对5号顶点进行入队操作,更新完毕后的dis数组与que队列为

 对二号顶点处理完毕后,进行出队操作,并对剩下的顶点进行相同的处理,知道队列为空为止,最终的dis与que数组状态为

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
int main(){
	int n, m;
	//u,v,w数组的大小要根据实际情况来定,要比m的最大值要大1.
	int u[8], v[8], w[8];
	//first要比n的最大值大1,next要比m的最大值大1.
	int first[6], next[8];
	int dis[6] = {0}, book[6] = {0};//book数组用来记录哪些顶点顶点已经在队列中
	int que[01] = {0}, head = 1, tail = 1; 
	//n表示顶点个数,m表示边的个数 
	cin >> n >> m;
	//初始化dis数组
	for (int i = 1; i <= n; ++i){
		dis[i] = inf;
	} 
	dis[1] = 0;
	//初始化first数组下标1~n的值为-1,表示1~n号顶点暂时都没有边
	for (int i = 1; i <= n; ++i){
		first[i] = -1;
	} 
	for (int i = 1; i <= m; ++i){
		//读入每一条边
		cin >> u[i] >> v[i] >> w[i];
		//下面两句是建立邻接表的关键
		next[i] = first[u[i]];
		first[u[i]] = i;
	}
	//first数组用来存储第几条边,用来循环找"桥" 
	//next数组我们来存放走完first数组这条边后,这条边的"桥"我们要走,
	//按照这个桥作为开头,继续去找其他的"桥",也就是next作为更新后的"开头"
	//1号顶点入队
	que[tail] = 1;
	tail++;
	book[1] = 1;//标记1号顶点已经入队
	while(head < tail){//队列不为空的时候循环
		int k = first[que[head]]; //当前需要处理的队首顶点
		while(k != -1){//扫描当前顶点的所有边
			if (dis[v[k]] > dis[u[k]] + w[k]){
				dis[v[k]] = dis[u[k]] + w[k];//更新1到顶点v[k]的路程
				if(book[v[k]] == 0){ // 表示不在队列中,将顶点v[k]1加入队列中
					que[tail] = v[k]; 
					tail++;
					book[v[k]] = 1;//同时标记顶点v[k]已经入队 
				} 
			} 
			k = next[k]; 
		} 
		book[que[head]] = 0; 
		head++;
	} 
	for (int i = 1; i <= n; ++i){
		cout << dis[i] << ' ';
	} 
	return 0;
}

 

 注:与未优化的对比,我们使用的是队列来进行的判断,在进行松弛判断方面,与未优化的对比也就是多了一个是否可以入队的操作

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值