补上之前欠了很久的总结
这次主要是总结最短路径问题,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->4与1->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),
- 即我们来判断dis[3]是否大于dis[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;
}
注:与未优化的对比,我们使用的是队列来进行的判断,在进行松弛判断方面,与未优化的对比也就是多了一个是否可以入队的操作