考研复试系列——第七节 最短路径
前言
前面我们学习了DFS算法,利用DFS算法,我们以每一个顶点为开始节点进行DFS,最后进行比较也可以求得最短路径,
但是复杂度不能满足我们的需求。现在我们通过Floyd算法和Dijkstra算法来解决最短路径问题。
Floyd算法
Floyd算法的思想很简单,就是借助第三个点来优化另外两个顶点的距离。比如我们有三个顶点A,B,C。三个顶点相互连接,我们要求A到C的
最短路径,肯定要比较A->B 和 A->C->B这两条路径的长度,后者即借助了第三个顶点C。我们现在用二维数组 e[ i ][ j ] 表示一个有向图,假如只
允许经过节点 1 来进行优化路径,我们只需要遍历节点,判断原有距离与经过节点 1 的距离的大小,然后若比原有的小则进行更新,代码如下:
//经过1号节点
for(i=1;i<=n;i++)//遍历二维数组
{
for(j=1;j<=n;j++)
{
if(e[i][j] > e[i][1] + e[1][j])//判断经过节点1是否距离更短
e[i][j] = e[i][1] + e[1][j];//更新二维数组(更新距离)
}
}
//经过2号节点
for(i=1;i<=n;i++)//遍历二维数组
{
for(j=1;j<=n;j++)
{
if(e[i][j] > e[i][2] + e[2][j])//判断经过节点2是否距离更短
e[i][j] = e[i][2] + e[2][j];//更新二维数组(更新距离)
}
}
//由上面的情况得到Floyd算法的核心代码
for(k=1;k<=n;k++)//最外层循环,表示经常哪个中间节点
{
for(i=1;i<=n;i++)//遍历二维数组
{
for(j=1;j<=n;j++)
{
if(e[i][j] > e[i][k] + e[k][j])//判断经过节点k是否距离更短
e[i][j] = e[i][k] + e[k][j];//更新二维数组(更新距离)
}
}
}
看起来很简单吧,但是要注意Floyd算法不能解决带有负权值环路的问题,假如存在负权值环路,那么每次绕一圈都减少的话,就无法找到最短路径了。
下面来一道例题:
第一行输入N,M (N<=100 , M <= 10000)其中N表示 N 个城市,M表示这些城市之间有M条路,接下来输入M行,每行三个整数A,B,C
(1<= A, B <= N , 1<= C <= 1000)表示城市A到B所需的距离。求标号为1的城市到标号为N的城市的最短距离。输入N,M为0 0 时结束。
思考:这道题目是一道典型的最短路径问题,且不含负权值(环路),我们可以使用Floyd算法或者迪杰斯特拉算法解决。现在先使用Floyd算法。
#include<iostream>
using namespace std;
int ans[101][101];//图的邻接矩阵
int main()
{
int N,M;
while(cin>>N>>M && N && M)
{
int i,j;
for(i=1;i<=N;i++)//邻接矩阵初始化
{
for(j=1;j<=N;j++)
{
ans[i][j] = 999999;//表示不可达
}
ans[i][i] = 0;//城市到自己的距离为0
}
int a,b,c;
for(i=1;i<=M;i++)//输入城市间距离
{
cin>>a>>b>>c;
ans[a][b] = ans[b][a] = c;//注意本题是无向图
}
int k;
for(k=1;k<=N;k++)//Floyd核心算法
for(i=1;i<=N;i++)
for(j=1;j<=N;j++)
if(ans[i][j] > ans[i][k] + ans[k][j])
ans[i][j] = ans[i][k] + ans[k][j];
cout<<ans[1][N]<<endl;//输出结果
}
return 0;
}
Dijkstra算法
//Dijkstra算法
/*
1 将所有顶点分为两个部分,已知最短路径的顶点集合P和未知最短路径的顶点集合Q。
最开始,集合P中只有源节点一个。我们可以使用一个数组来表示对于某个顶点i,它
是否存在于集合p中,数组[i]为1表示在,为0表示不在。
2 用dis数组来记录源点到各顶点的距离,初始时设源点s到自己的距离为0,即dis[s] = 0
若存在源点能够直接到达的顶点i,则设置dis[i] = e[s][i]。设置不可达的为无穷
3 在集合Q的所有顶点中选择一个离源点S最近的顶点u (即dis[u]最小)加入到集合P中,并考察
所有以点u为起点的边,对每一条边执行如下操作:
如果存在一条从u到v的边,判断dis[u]+e[u][v]与dis[v]的大小,若前者小,则更新dis[v]为
dis[u]+e[u][v]。
4 重复执行第三步,如果集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径
*/
我们使用Dijkstra算法来解决上面的问题
#include<iostream>
using namespace std;
int ans[101][101];//图的邻接矩阵
int book[101];//标记是否在集合P中
int dis[101];//保存距离
int main()
{
int N,M;
while(cin>>N>>M && N && M)
{
int i,j;
for(i=1;i<=N;i++)//邻接矩阵初始化
{
for(j=1;j<=N;j++)
{
ans[i][j] = 999999;//表示不可达
}
ans[i][i] = 0;//城市到自己的距离为0
}
int a,b,c;
for(i=1;i<=M;i++)//输入城市间距离
{
cin>>a>>b>>c;
ans[a][b] = ans[b][a] = c;//注意本题是无向图
}
for(i=1;i<=N;i++)//dis数组初始化
dis[i] = ans[1][i];
for(i=1;i<=N;i++)//初始化book数组
book[i] = 0;
book[1] = 1;//结点1为源点
for(i=1;i<=N-1;i++)//Dijkstra核心算法
{
//在集合Q中寻找距离源点最近的顶点u
int min = 99999,u,v;
for(j=1;j<=N;j++)
{
if(book[j] == 0 && dis[j] < min)
{
min = dis[j];
u = j;
}
}
book[u] = 1;//将顶点u加入集合P
for(v=1;v<=N;v++)//判断与更新dis数组
{
if(dis[v] > dis[u] + ans[u][v])
dis[v] = dis[u] + ans[u][v];
}
}
cout<<dis[N]<<endl;
}
return 0;
}
再来一道上述题目的变形:
给你n个点,m条无向边,每条边都有长度d和花费p ,给你起点s和终点t,要求输出起点到终点的最短距离及其花费。如果
最短距离有多条路线,则输出花费最少的。
输入n,m,点的编号是1到n,然后是m行,每行4个数a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p。
最后一行是两个数s,t起点s终点t。n和m为0时输入结束。(1< n <= 1000 , 0< m < 100000 ,s != t)
sample input:
3 2
1 2 5 6
2 3 4 5
1 3
0 0
sample output:
9 11
思考:这道题目和上一道大体一样,只是多了花费,我们只要处理好花费就可以了,且我们可以看到花费和距离的性质是相同的,我们可以采取
相同的处理策略,只是在距离一致时,判断花费的多少即可。详情见代码:
#include<iostream>
using namespace std;
int ans[1001][1001];//保存有向图距离
int anm[1001][1001];//保存有向图花费
int book[1001];//标记顶点是否在集合P中
int dis[1001];//保存距离
int money[1001];//保存花费
int main()
{
int n,m;
while(cin>>n>>m && n && m)
{
int a,b,d,p;
int i,j;
for(i=1;i<=1000;i++)//距离以及花费数组的初始化
{
for(j=1;j<=1000;j++)
ans[i][j] = anm[i][j] = 99999;
ans[i][i] = anm[i][i] = 0;
}
for(i=1;i<=m;i++)//输入数据
{
cin>>a>>b>>d>>p;
ans[a][b] = ans[b][a] = d;//注意是无向图
anm[a][b] = anm[b][a] = p;
}
int s,t;
cin>>s>>t;//输入起点和终点
for(i=1;i<=n;i++)//dis和money以及book数组初始化
{
dis[i] = ans[s][i];
money[i] = anm[s][i];
book[i] = 0;
}
book[s] = 1;//将源点加入到集合P
for(i=1;i<=n-1;i++)//选择其余n-1个节点依次加入到集合P直到集合P包含所有节点
{
int min = 999999,u,v;
for(j=1;j<=n;j++)//从集合Q中寻找一个距离源点最小的节点
{
if(book[j] == 0 && dis[j] < min)
{
min = dis[j];
u = j;
}
}
book[u] = 1;//加入节点u
for(v=1;v<=n;v++)//以u为中转,判断源点到节点v的情况与更新
{
if(dis[v] > dis[u] + ans[u][v])//经过节点u距离变小了
{
dis[v] = dis[u] + ans[u][v];//更新距离
money[v] = money[u] + anm[u][v];//更新花费
}
else if(dis[v] == dis[u] + ans[u][v])//如果最小距离相等
{
if(money[v] > money[u] + ans[u][v])//选择花费更小的
money[v] = money[u] + anm[u][v];
}
}
}
cout<<dis[t]<<" "<<money[t]<<endl;
}
return 0;
}
注意:如果对于空间有要求的话可以使用结构体来保存顶点和花费以及距离信息。
对于考研复试,掌握这些已经达到基本要求了,对于负权值边如何处理,感兴趣的可以去看下Bellman-Ford算法。