求最短路径的方法,目前笔者还在进一步学习中,今天先来总结一下笔者已经学习到的方法,算是进一步复习和巩固这些算法:
注:三种算法的题解均为PTA 7-10 旅游规划
1.Dijkstra算法:
该算法可与Floyd算法互通,但是略有不同,Dijkstra算法重在解决给定源点(即出发点)的最短路径算法,主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。首先找到一个没有确定最短路且距离起点最近的点,并通过这个点将其他点的最短距离进行更新。每做一次这个步骤,都能确定一个点的最短路,所以需要重复此步骤 n 次,找出 n 个点的最短路。
还是老规矩,用一道经典的例题来进一步分析和理解该算法:
7-10 旅游规划
有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。
输入格式:
输入说明:输入数据的第1行给出4个正整数N、M、S、D,其中N(2≤N≤500)是城市的个数,顺便假设城市的编号为0~(N−1);M是高速公路的条数;S是出发地的城市编号;D是目的地的城市编号。随后的M行中,每行给出一条高速公路的信息,分别是:城市1、城市2、高速公路长度、收费额,中间用空格分开,数字均为整数且不超过500。输入保证解的存在。
输出格式:
在一行里输出路径的长度和收费总额,数字间以空格分隔,输出结尾不能有多余空格。
首先来分析题目,典型的点到点的最短路径,首先确定思路,给出了出发点是s和终点d,首先要设一个二维数组G,且G[i][j]表示的是这条路的状态(花费和长度),又因为要一个点一个点的逐个加入最短路径的计算中,这里还要分别计算起点到单个节点的最短路径,所以还要单设一个一维数组d,其中d[i]表示起点到i节点的最短路径,在使用Dijkstra算法过程中,不断更新d[i]的值,每次取最小值,知到最后选完节点结束,(这里有个有趣的问题,Dijkstra算法其实是你给我一个起点,我能给你算出来到其他n-1个顶点的n-1个最短距离的值,也就是最后被存储在d[]中的数,对于一些终点确定的题目来说,浪费了一些时间,但是这是目前为止最好的办法,还没有研究出其他的可以改善的方法),接下来我会在代码块的注释里详细解释过程:
#include <bits/stdc++.h>
#define maxm 505
#define M 0x3f3f3f3f//相当于设置的无穷大数量级
using namespace std;
int n,m;//分别代表城市个数和公路条数
struct node
{
int len;
int cost;
}G[maxm][maxm];
int visited[maxm];//用于表示节点是否被访问
int d[maxm];//用于存储最短路径长度
int v[maxm];//用于存储最短路径花费
void Dijkstra(int s)//传入起点,终点不需要传入
{
//第一步首先找出与起点相邻的若干节点将其d[]和v[]修改
for(int i=0; i<n; i++)
{
d[i]=G[s][i].len;
v[i]=G[s][i].cost;
}
visited[s]=1;//先将起点加入集合进入循环时就可以从起点开始找边
d[s]=0;
v[s]=0;
int k=0;
while(k<n)//找n-1次
{
int minidx=-1;
//int mincost=M;
int minlen=M;
for(int i=0; i<n; ++i)
{
if(!visited[i]&&d[i]<minlen)//注意此处要找到最小的那条边,所以minlen要在循环结束前保持更新
{
minlen=d[i];
minidx=i;
}
}
if(minidx==-1)
break;//判断是不是非连通图,此题样例中未考虑此种情况,故本题没有写出这种情况
visited[minidx]=1;
for(int i=0; i<n; i++)
{
if(!visited[i]&&(d[minidx]+G[minidx][i].len<d[i]))//如果加入的这个点后起点可以通过这个点到达以前不能到达的距离,也就是相当于最短距离不再是无穷大了了
{
d[i]=d[minidx]+G[minidx][i].len;
v[i]=v[minidx]+G[minidx][i].cost;
}
else if(!visited[i]&&(d[minidx]+G[minidx][i].len==d[i])&&(v[minidx]+G[minidx][i].cost<v[i]))//如果出现路程相等但是花费少的情况要更新最小值,但是d不用更新,如果需要输出经过的路径的话就要更新了
v[i]=v[minidx]+G[minidx][i].cost;
}
k++;
}
}
int main()
{
int _s,_d;//起点和终点
cin>>n>>m>>_s>>_d;
memset(visited,0,sizeof(visited));
for(int i=0; i<n; ++i)
for(int j=0; j<n; ++j)
{
G[i][j].len=M;
G[i][j].cost=M;
}
for(int i=0; i<m; ++i)
{
int ss,dd,lenn,costt;
cin>>ss>>dd>>lenn>>costt;
G[ss][dd].len=G[dd][ss].len=lenn;
G[ss][dd].cost=G[dd][ss].cost=costt;//注意此处为无向图
}
Dijkstra(_s);
cout<<d[_d]<<" "<<v[_d]<<endl;
return 0;
}
/*
4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20
*/
注意点:1.如果本题要输出最短的路径的话,要继续定义一个字符串数组用来保存和更新路径,我在上篇题解的Floyd 算法例题中写出了处理方法,如有需要可以参考http://t.csdn.cn/feyuT
2.Floyd 算法
Floyd算法是基于动态规划的,从结点 i 到结点 j 的最短路径只有两种:
1、直接 i 到 j
2、i 经过若干个结点到 k 再到 j
对于每一个k,我们都判断 d[i][j] 是否大于 d[i][k] + d[k][j],如果大于,就可以更新d[i][j]了。
例题来喽~
其实我觉得上个例题可以用Floyd算法,所以我打算试一试,,,此处作者正在努力写代码,
after a period of time~~~(额,拽个英语缓解下气氛),咳咳,步入正题
#include <bits/stdc++.h>
#define maxm 505
#define M 0x3f3f3f3f//相当于设置的无穷大数量级
using namespace std;
/*
int n,m;//分别代表城市个数和公路条数
struct node
{
int len;
int cost;
}G[maxm][maxm];
int visited[maxm];//用于表示节点是否被访问
int d[maxm];//用于存储最短路径长度
int v[maxm];//用于存储最短路径花费
void Dijkstra(int s)//传入起点,终点不需要传入
{
//第一步首先找出与起点相邻的若干节点将其d[]和v[]修改
for(int i=0; i<n; i++)
{
d[i]=G[s][i].len;
v[i]=G[s][i].cost;
}
visited[s]=1;//先将起点加入集合进入循环时就可以从起点开始找边
d[s]=0;
v[s]=0;
int k=0;
while(k<n)//找n-1次
{
int minidx=-1;
//int mincost=M;
int minlen=M;
for(int i=0; i<n; ++i)
{
if(!visited[i]&&d[i]<minlen)
{
minlen=d[i];
minidx=i;
}
}
if(minidx==-1)
break;//判断是不是非连通图
visited[minidx]=1;
for(int i=0; i<n; i++)
{
if(!visited[i]&&(d[minidx]+G[minidx][i].len<d[i]))//如果加入的这个点后起点可以通过这个点到达以前不能到达的距离,也就是相当于最短距离不再是无穷大了了
{
d[i]=d[minidx]+G[minidx][i].len;
v[i]=v[minidx]+G[minidx][i].cost;
}
else if(!visited[i]&&(d[minidx]+G[minidx][i].len==d[i])&&(v[minidx]+G[minidx][i].cost<v[i]))//如果出现路程相等但是花费少的情况要更新最小值,但是d不用更新,如果需要输出经过的路径的话就要更新了
v[i]=v[minidx]+G[minidx][i].cost;
}
k++;
}
}
int main()
{
int _s,_d;//起点和终点
cin>>n>>m>>_s>>_d;
memset(visited,0,sizeof(visited));
for(int i=0; i<n; ++i)
for(int j=0; j<n; ++j)
{
G[i][j].len=M;
G[i][j].cost=M;
}
for(int i=0; i<m; ++i)
{
int ss,dd,lenn,costt;
cin>>ss>>dd>>lenn>>costt;
G[ss][dd].len=G[dd][ss].len=lenn;
G[ss][dd].cost=G[dd][ss].cost=costt;//注意此处为无向图
}
Dijkstra(_s);
cout<<d[_d]<<" "<<v[_d]<<endl;
return 0;
}
*/
/*
4 5 0 3
0 1 1 20
1 3 2 30
0 3 4 10
0 2 2 20
2 3 1 20
*/
int n,m,_s,_d;
struct node
{
int len;
int cost;
}G[maxm][maxm];
int visited[maxm];
int v[maxm][maxm];
int d[maxm][maxm];
void floyd()
{
for(int j=0;j<n;j++)//首先还是赋初值
for(int i=0;i<n;i++)
{
d[j][i]=G[j][i].len;
v[j][i]=G[j][i].cost;
}
for(int k=0;k<n;k++)//当插入第k个节点时看看会不会使i到j的变小
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
if(d[i][j]>d[i][k]+d[k][j])
{d[i][j]=d[i][k]+d[k][j];v[i][j]=v[i][k]+v[k][j];}//如果加入节点k之后i到j的路径变短,则无需考虑花费多少,直接计入最短路径
else if(d[i][j]==d[i][k]+d[k][j]&&v[i][j]>v[i][k]+v[k][j])//如果两种情况相同,则要考虑最小花费
v[i][j]=v[i][k]+v[k][j];
}
}
int main()
{
cin>>n>>m>>_s>>_d;
memset(visited,0,sizeof(visited));
for(int i=0; i<n; ++i)
for(int j=0; j<n; ++j)
{
G[i][j].len=M;
G[i][j].cost=M;
}
for(int i=0; i<m; ++i)
{
int ss,dd,lenn,costt;
cin>>ss>>dd>>lenn>>costt;
G[ss][dd].len=G[dd][ss].len=lenn;
G[ss][dd].cost=G[dd][ss].cost=costt;//注意此处为无向图
}
floyd();
cout<<d[_s][_d]<<" "<<v[_s][_d]<<endl;
return 0;
}
还是有些注意点,可以看到floyd算法比Dijkstra算法省去了一个visited数组,也就是可以不在标记节点是不是已经被访问,但是取而代之的是引入一个二维数组,所以在节点数上限很高的情况下可能会超出空间,所以要看清题目要求的内存在决定使用哪种算法,但是floyd算法也有优势,就是代码优雅,一个三层嵌套的for循环就能解决问题,如果此处不需考虑花费的话代码更加简洁,只一行min即可,简直不要太爽。
3.SPFA算法
其实这个算法我也比较陌生,但是前面两种算法可以看到时间复杂度都为n的三次方,效率比较低,所以说我就上网查找性能更高的算法,下面介绍我学到的一种SPFA算法
SPFA算法需要图中没有负环才能使用。其实大部分正权图也是可以用SPFA算法做的,效率一般高于Dijkstra算法。
基于一般原理就是注意到前面两种算法都做了很多无用的操作,其实在每次遍历寻找最小边加入最短路的时候我们只需要关注上次被更新了的节点即可,没有更新的节点不会对此次的寻找产生影响,我们只需要拿更新过后的点去更新其他的点,因为只有用被更新过的点更新其他结点x,x的距离才可能变小,所以可以在遍历过程中,将节点加入后改变d和v数组的节点依次加入队列,在之后的便利中,只是遍历这些队列中的节点即可,不必再全部遍历一遍,但是要注意在遍历完成后再将节点“释放”,即恢复未访问未访问状态,直到找不到最短路时队列的不会再加入元素,直到遍历完队列的元素,也就找到了最短路。
#include <bits/stdc++.h>
#define maxm 505
#define M 0x3f3f3f3f//相当于设置的无穷大数量级
using namespace std;
int n,m,_s,_d;
struct node
{
int len;
int cost;
} G[maxm][maxm];
int visited[maxm];
int v[maxm];
int d[maxm];
void SPFA(int s)
{
//第一步首先要赋最大值,这里不能更新起点的临接节点距离,因为更新之后会对之后队列在选择第一次入队节点时发生差错,第一次更新具体值要在队列的循环里实现
for(int i=0; i<n; i++)
{
d[i]=M;
v[i]=M;
}
visited[s]=1;//先将起点加入集合进入循环时就可以从起点开始找边
d[s]=0;
v[s]=0;
queue<int> p;
p.push(s);
while(!p.empty())
{
int t=p.front();
p.pop();
visited[t]=0;//边权可能存在负数的情况,要释放这个边供接下来使用
for(int i=0; i<n; i++)
{
if(d[i]>d[t]+G[t][i].len)
{
d[i]=d[t]+G[t][i].len;
v[i]=v[t]+G[t][i].cost;
if(!visited[i])
{
p.push(i);
visited[i]=1;
}
}
else if(d[i]==d[t]+G[t][i].len&&v[i]>v[t]+G[t][i].cost)
{
v[i]=v[t]+G[t][i].cost;
if(!visited[i])//如果i不在队列里,则i入队
{
p.push(i);
visited[i]=1;
}
}
}
}
if(d[_d]==M)
cout<<"没有最短路"<<endl;
else
cout<<d[_d]<<" "<<v[_d]<<endl;
}
int main()
{
cin>>n>>m>>_s>>_d;
memset(visited,0,sizeof(visited));
for(int i=0; i<n; ++i)
for(int j=0; j<n; ++j)
{
G[i][j].len=M;
G[i][j].cost=M;
}
for(int i=0; i<m; ++i)
{
int ss,dd,lenn,costt;
cin>>ss>>dd>>lenn>>costt;
G[ss][dd].len=G[dd][ss].len=lenn;
G[ss][dd].cost=G[dd][ss].cost=costt;//注意此处为无向图
}
SPFA(_s);
return 0;
}
4.Bellman - ford算法
先来简单介绍一下该算法,该算法最特殊的一点是他能方便地解决有边数限制的单源最短路径算法,Bellman-Ford算法是通过循环 n 次,每次循环都遍历每条边,进而更新结点的距离,每一次的循环至少可以确定一个点的最短路,所以循环 n次,就可以求出 n 个点的最短路。对于如何加入边数限制的条件,本质上,假设边数限制是k,那么本来经过k+1条边能到达的点就应该被视为不可达,而解决这一个问题的限制就是用更新本次节点的上一次节点来判断当前前节点是否有最小状态,举个例子加入k=1,且1->2能直接到达,但是2->3也能直接到达,但是如果是求1->3的最短路径显然两条边的最短路径不符合要求,于是我们可以用在1节点时的d[3]=无穷来表示此时d[3]的状态,也就是选取完2节点之前的状态,但是因为d数组也要时时保持更新,它不能一直保存前一个节点下的状态,所以还要设一个数组来记录该节点还没有选择时的状态,在比较时就比较当前选取节点和上一个状态的值,取最小即可,注意要在选取不超过k条边的情况下,所以外面的for循环要循环k次,内层就是遍历m条边
例题(专门从网上找的):
853. 有边数限制的最短路
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数n,m,k。
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。
如果不存在满足条件的路径,则输出“impossible”。
数据范围
1≤n,k≤500,
1≤m≤10000 ,
任意边长的绝对值不超过10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 510,M = 10010;
int n,m,k;
int dist[N],backup[N]; //backup数组为上次
struct edges
{
int a,b,w; // a->b权值为w的边
}edge[M];
int bellman_ford()
{
memset(dist,0x3f,sizeof(dist));//全都标记成最大值
dist[1] = 0;
for(int i=0; i<k; i++) //若是在k次之内还是没找到,返回没有最短路径
{
memcpy(backup,dist,sizeof(dist)); //备份上一次的dist数组状态
for(int j=0; j<m; j++) // 枚举所有边
{
int a = edge[j].a, b = edge[j].b, w=edge[j].w;
dist[b] = min(dist[b],backup[a]+w); // 用备份更新
}
}
if(dist[n] > 0x3f3f3f3f/2) return -1;
return dist[n];
}
int main()
{
cin >> n >> m >> k;
for(int i=0; i<m; i++)
{
int a,b,w;
cin >> a >> b >> w;
edge[i] = {a,b,w};
}
int t = bellman_ford();
if(t == -1) cout << "impossible";
else cout << t;
return 0;
}
注:原题转载自http://t.csdn.cn/rkbek
好了,这次的分享就到这里了,以上言语若有纰漏,还望指正,但是代码都是经作者亲测AC代码,大可放心“食用”!