数据结构与算法-图论(三)-最短路径

时间:2024-04-11 08:12:09

数据结构与算法-图论(三)-最短路径

​ 在讨论完最小生成树后,我们再来了解图论中另一个经典问题:最短路径问题。即寻找图中某两个特定结点间最短的路径长度。所谓图上的路径,即从图中一个起始终点到一个终止终点途中经过的所有结点序列,路径的长度即所经过的边权和。

数据结构与算法-图论(三)-最短路径

​ 最短路径问题在实际中的应用也非常广泛,例如确定某两个城市间的最短行车路线长度。要解决这类问题,我们要根据图的特点和问题的特征选择不同的算法。

我们首先来介绍第一种计算最短路径长度的算法-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])  
                     e[i][j]=e[i][k]+e[k][j];

这段代码的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。

题目描述:

在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的t-shirt。但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,非常累!所以现在他们想要寻找最短的从商店到赛场的路线,你可以帮助他们吗?

输入:

输入包括多组数据。每组数据第一行是两个整数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条商店到赛场的路线。当输入为两个0时,输入结束。

输出:

对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间。

样例输入:

2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0

样例输出:

3
2

代码如下:

#include <iostream>
#include <cstdio>

using namespace std;

#define 101 N
int ans[N][N];

int main(){
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF){
        if(n == 0 && m == 0) break;
        for(int i = 1 ; i <= n ; i++){
            for(int j = 1 ; j <= n ; j++){
                ans[i][j] = -1;	// 对邻接矩阵初始化,我们用-1代表无穷
            }
            ans[i][i] = 0;	// 自己到自己的路径长度设为0
        }
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            ans[a][b] = ans[b][a] = c;	// 对邻接矩阵赋值,由于是无向图,该赋值操作要进行两次
        }
        for(int k = 1 ; k <= n ; k++){
            for(int i = 1 ; i <= n ; i++){
                for(int j = 1 ; j <= n ; j++){
                    if(ans[i][k] == -1 || ans[k][j] == -1)
                        continue;	// 若两值中有一个值为无穷,则ans[i][j]不能由于经过结点k而被更新,跳过循环,保持原值
                    if(ans[i][j] == -1 || ans[i][k] + ans[k][j] < ans[i][j])
                        	ans[i][j] = ans[i][k] + ans[k][j];
                    // 当由于进过k可以获得更短的最短路径时,更新该值
                }
            }
        }
        printf("%d\n",ans[1][n]);	// 循环结束后输出答案
    }
    return 0;
}

运行结果:

数据结构与算法-图论(三)-最短路径

对于Floyd算法具体理解可以参考这篇博客:https://www.cnblogs.com/wangyuliang/p/9216365.html

​ 我们总结Floyd算法的特点。第一,牢记其时间复杂度为O(N^3),所以在大部分机试题的时间允许范围内,他要求被求解图的大小不大于200个结点,若超过该数字该算法很可能因为效率不够高而被判超时。第二,Floyd算法利用一个二维矩阵来进行相关运算,所以当图使用邻接矩阵表示时更为方便。若原图并非由邻接矩阵给出时我们设法将其转换,注意当两个节点间有多余一条边时,选择长度最小的边权值存入邻接矩阵。第三,当Floyd算法完成后,图中所有结点之间的最短路径都将被确定。所以,其较适用于要求询问多个结点对之间的最短路径长度问题,即全源最短路径问题。了解它的这些特点,将有利于我们在特定问题中决定是否使用Floyd算法。

​ 在讨论完Floyd算法后,我们来看另一种与其特点完全不同的最短路径算法-Dijkstra算法(迪杰斯特拉算法)。它与之间讨论的Floyd算法有一个非常明显的区别,Floyd算法可以计算出图中所有结点对之间的最短路径长度,Dijkstra算法只能求得某特定结点到其他所有结点的最短路径长度,即单源最短路径问题。

关于Dijstra算法的具体理解可以参考这篇博客:https://www.cnblogs.com/wangyuliang/p/9216511.html

我们使用Dijstra算法重写上例:

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

struct E{		// 邻接链表中链表元素结构体
    int next;	// 代表直接相邻的结点
    int c;		// 代表该边的权值(长度)
};
vector<E> edge[101];	// 邻接链表
bool mark[101];	// 标记,当mark[j]为true时表示结点j的最短路径长度已经得到,该结点已经加入集合K
int Dis[101];	// 距离向量,当mark[i]为true时,表示已得的最短路径长度;否则,表示所有从结点1出发,经过已知的最短路径达到集合K中的某结点
int main(){
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF){
        if(n == 0 && m == 0) break;
        for(int i = 1 ; i <= n ; i++) edge[i].clear();	// 初始化邻接链表
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            E tmp;
            tmp.c = c;
            tmp.next = b;
            edge[a].push_back(tmp);
            tmp.next = a;
            edge[b].push_back(tmp);
            // 将邻接信息加入邻接链表,由于原图为无向图,固每条边信息都要添加到其两个顶点的两条单链表中
        }
        for(int i = 1 ; i <= n ; i++){
            Dis[i] = -1;	// 所有距离为-1,即不可达
            mark[i] = false;	// 所有结点不属于集合K
        }
        Dis[1] = 0;	// 得到最近的点为结点1,长度为0
        mark[1] = true;	// 将结点1加入集合K
        int newP = 1;	// 集合K中新加入的点为结点1
        for(int i = 1 ; i < n ; i++){// 循环n-1次,按照最短路径递增的顺序确定其他n-1个点的最短路径长度
            for(int j = 0 ; j < edge[newP].size() ; j++){
                int t = edge[newP][j].next;	// 该边的另一个结点
                int c = edge[newP][j].c;	// 该边的长度
                if(mark[t] == true) continue;	// 若另一个结点也属于集合K,则跳过
                if(Dis[t] == -1 || Dis[t] > Dis[newP]+c)	// 若该结点尚不可达,或者该结点从新加入的结点经过一条边到达时比以往距离更短
                    Dis[t] = Dis[newP] + c;
            }
            int min = 123123123;	// 最小值初始化为一个大整数,为找最小值做准备
            for(int j = 1 ; j <= n ; j++){// 遍历所有结点
                if(mark[j] == true) continue;
                if(Dis[j] == -1) continue;
                if(Dis[j] < min){
                    min = Dis[j];
                    newP = j;	// 新加入的点暂定为该点
                }
            }
            mark[newP] = true;
        }
        printf("%d\n",Dis[n]);
    }
}

运行结果:

数据结构与算法-图论(三)-最短路径

如代码所示,我们用mark[i]的值来确定结点i的最短路径是否已经被确定,并根据结点i是否属于集合K,Dis[i]中的数字体现出不同的意义。当结点i属于集合K时,Dis[i]代表结点i已经被确定的最短路径长度;相反,若结点i不属于集合K,那么Dis[i]表示,所有从结点1出发先按照某条最短路径到达已经在集合K中的结点,并由该结点经过一条边到达结点i路径中的最短距离。

总之就是将新加入进K集合的点的所有出边进行“松弛”,然后在“松弛”完后的非K集合中寻找距离最小的点加入集合K,并将新加入的点标记。

我们再来看一个例题:

题目描述:

给你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时输入结束。

输出:

输出一行有两个数:最短距离及其花费。

样例输入:

3 2
1 2 5 6
2 3 4 5
1 3
0 0

样例输出:

9 11

​ 在该例中,我们不仅需要求得起点到终点的最短距离,还需要在有多条最短路径时,选取花费最少的那一条。要解决这个问题,只要更改Dijstra算法中关于“更近”的评判标准即可:有两条路径,若他们距离不一样时,距离小的更近;若距离一样时花费少的更近。当定义这种新的评判标准后,Dijstra算法照样能为我们求得“最近”的路径长度。

代码如下:

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

struct E{// 邻接链表元素结构体
    int next;
    int c;
    int cost;
};
vector<E> edge[1001];	// 邻接链表
int Dis[1001];			// 距离数组
int cost[1001];			// 花费数组
bool mark[1001];		// 是否属于集合K数组

int main(){
    int n,m;
    int S,T;	// 起点,终点
    while(scanf("%d%d",&n,&m)!=EOF){
        if(n == 0 && m == 0) break;
        for(int i = 1 ; i <= n ; i++) edge[i].clear();	// 初始化邻接链表
        while(m--){
            int a,b,c,cost;
            scanf("%d%d%d%d",&a,&b,&c,&cost);
            E tmp;
            tmp.c = c;
            tmp.cost = cost;
            tmp.next = b;
            edge[a].push_back(tmp);
            tmp.next = a;
            edge[b].push_back(tmp);
        }
        scanf("%d%d",&S,&T);
        for(int i = 1 ; i <= n ; i++){
            Dis[i] = -1;
            mark[i] = false;
        }
        Dis[S] = 0;
        mark[S] = true;
        int newP = S;	// 起点为S,将其加入集合K,且其最短距离确定为0
        for(int i = 1 ; i < n ; i++){
            for(int j = 0 ; j < edge[newP].size(); j++){
                int t = edge[newP][j].next;
                int c = edge[newP][j].c;
                int co = edge[newP][j].cost;	// 花费
                if(mark[t] == true) continue;
                if(Dis[t] == -1 || Dis[t] > Dis[newP] + c || Dis[t] == Dis[newP] + c && cost[t] > cost[newP]+co){
                    Dis[t] = Dis[newP] + c;
                    cost[t] = cost[newP] + co;
                }
            }
            int min = 123123123;
            for(int j = 1 ; j <= n ; j++){
                if(mark[j] == true) continue;
                if(Dis[j] == -1) continue;
                if(Dis[j] < min){
                    min = Dis[j];
                    newP = j;
                }
            }
            mark[newP] = true;
        }
        printf("%d %d\n",Dis[T],cost[T]);	// 输出答案
    }
    return 0;
}

运行结果:

数据结构与算法-图论(三)-最短路径