图论及其应用——图的最短路径问题
基于前文对图和树的简单讨论,我们在这篇文章中将介绍有关图的最短路径的问题。
最短路径的原始模型非常简单,给出一个图G(V、E),其中边元素e都带有权值,寻找vi和vj之间的一条路径,是的该路径上边的权值之和最小。
基于这个最简单的模型,最短路径问题细分还会有好多种类,比如图G是否有向?权值是否有负值?比如是单源头最短路还是多源头?
在这里,我们先讨论利用Dijkstra算法实现的单源无负权值无向图的最短路问题。
考察无向图G<V,E>,点集的元素个数是n,我们利用二维数组map[i][j]记录无向图中边vivj的权值,如果vi与vj不连通,那么map[i][j]无限大。单源是s,并设置dis[j]记录单源s到j的最短路径(也就是最小的权值之和)。
我们考察s点到vj的最短路径,假设是<s、v1、v2……vj-1、vj>是s到vj的最短路径,那么用反证法很容易证明,<s、v1、v2……vj-1>是s到vj-1的最短路径,那么对于s到vj的最短路径的求解,我们可以看成这样一个过程,在与vj直接相连的点vm、vn、vp中(这里假设与vj直接相连的只有这三个点),那么我们只需比较dis[m] + map[m][j] 、 dis[n] + map[n][j]、dis[p] + map[p][j]中的最小值,即是s到vj的最短路径。如果我们用动态规划的思想去解读这个过程,会得到这样一个状态转移方程:
dis[j] = min(dis[m] + map[m][j] , dis[n] + map[n][j],dis[p] + map[p][j] , ……),其中vm、vn、vp……表示点集V中和vj直接相连的点。
那么基于这种递推关系,我们就很容易考虑如何用程序化的算法来实现最小路径的求解了,可以看到,s到vj的最短路径是要基于较短的最短路径,因此我们需要从较短的最短路径开始构造。
首先我们从与源头s直接相连的点集v1开始,找到一条边,并标记顶点v1下次不再访问,使得<s、v1>的权值最小,这便是s到v1所有边数为1的路径中的最短路径,那么基于这最短路径,便可以逐层的往上构造边数更长的最短路径。
Dijkstra算法基于无负权的图,因此从源头s到与其直接相连的点的最短路径是不会存在中转点的。
由于我们我每次从源头s开始构造最短路径,至少会完成一个点的最短路径的构造,那么对于含有n个元素的点集,显然我们需要构造n-1次,才可以确保找到源头s到G中每一个点的最短路径都记录在dis[]当中,那么如果想访问s到vj的最短路径的最小权值,只需输出dis[j]即可。
我们通过一个题目来进一步体会一下Dijkstra算法的实现。(Problem source : hdu 2066)
数理分析:基于单源最短路径问题的模型,这里其实是给出了多个单源(和小草相连的城市,由于题目没给相关数据,显然这里认为小草的家到源头是不花时间的),也就是需要进行多次Dijkstra算法,然后维护一个最小值即可。
基于对Dijkstra算法思想的理解,不难进行编程实现。
参考代码如下。
#include<iostream> #include<cstdio> #include<string.h> #include<algorithm> using namespace std; #define MAX 0x3f3f3f3f int road , link ,want , total; int Map[1010][1010] , linkarr[1010] , wantarr[1010] , dis[1010]; bool visit[1010]; void Dijkstra(int start) { int temp , k; memset(visit , 0 , sizeof(visit)); for(int i = 1;i <= total;++i) dis[i] = Map[start][i]; dis[start] = 0; visit[start] = 1; for(int i = 1;i <= total;++i) { temp = MAX; for(int j = 1;j <= total;++j) if(!visit[j] && temp > dis[j]) temp = dis[k = j]; visit[k] = 1; for(int j = 1;j <= total;++j) if(!visit[j] && dis[j] > dis[k] + Map[k][j]) dis[j] = dis[k] + Map[k][j]; } } int main() { int x , y , cost , minn , answer; while(scanf("%d%d%d",&road,&link,&want) != EOF) { total = 0; memset(Map , MAX ,sizeof(Map)); for(int i = 1;i <= road;++i) { scanf("%d%d%d",&x,&y,&cost); if(cost < Map[x][y]) //这里是个小坑,在记录图的信息的时候,会出现平行边,直接记录较小的边即可 Map[x][y] = Map[y][x] = cost; total = max(total , max(x , y)); } for(int i = 1;i <= link;++i) scanf("%d",&linkarr[i]); for(int i = 1;i <= want;++i) scanf("%d",&wantarr[i]); answer = MAX; for(int i = 1;i <= link;++i) //遍历与小草家相连的城市并以其为起点进行Dijkstra算法 { Dijkstra(linkarr[i]); minn = MAX; for(int j = 1;j <= want;++j) if(dis[wantarr[j]] < minn) minn = dis[wantarr[j]]; if(answer > minn) answer = minn; } printf("%d\n",answer); } }
我们曾提到,解决图中最短路径的几个经典算法Dijkstra、SPFA等算法,适用于解决单源最短路径的问题,那么对于一个图G,我们想知道任意两点vi到vj的最短路径,该如何求解呢?
下面我们就来介绍求解每对顶点间的最短距离的Floyd算法。
我们容易看到,对着求解任意两点之间的最短距离,我们肯定是要基于穷举算法来遍历到所有的情况。显然我们需要两层循环来遍历起始点i和终止点j,我们再设置一个中间点k,表示vi到vj的路径中会经过vk。
我们反身来看待这个问题,需要得到所有情况,需要找到最优解,容易看到,这正是动态规划能够解决的问题,因此我们很自然的联想到利用动态规划的思想来解决这一模型。
我们设置二维数组dp[i][j]来表示vi到vj的最短路径,我们容易得到如下的状态转移方程:
dp[i][j] = min(dp[i][j],dp[i][k] + dp[k][j]),其中k要遍历vi、vj路径上的所有点。
通过这样一个状态转移方程,我们就能够遍历出所有的情况并且找到每种情况的最优解了。
我们根据一个具体的题目来编程实现一下Floyd算法。(Problem source : hdu 1596)
数理分析:其实这个题目是在上文我们引入Floyd算法的模型的基础上稍作了改动,比如说在这个问题中并不是求解最短路径而是最长路径,而且这个这个“长”代表路径中边的权值的乘积,可以看到,思路是完全一样的,只不过是在状态转移方程上稍作修饰即可。
即dp[i][j] = max(dp[i][j] , dp[i][k]*dp[k][j]).
参考代码如下。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 1005; int n; double Map[maxn][maxn]; double s[maxn][maxn]; void F_W() { for(int k = 1 ;k <= n;k++) for(int i = 1;i <= n;i++) for(int j = 1;j <= n ;j++) s[i][j] = max(s[i][j] , s[i][k]*s[k][j]); } int main() { while(scanf("%d",&n) != EOF) { int i , j; for(i = 1;i <= n;i++) for(j = 1;j <= n;j++) { scanf("%lf",&Map[i][j]); s[i][j] = Map[i][j]; } F_W(); int num; scanf("%d",&num); int S , E; while(num--) { scanf("%d%d",&S,&E); if(s[S][E] > 0) printf("%.3lf\n",s[S][E]); else printf("What a pity!\n"); } } }