有些时候需要求一个图中两点间的最短路径,在不带边权值时一般是求解最短(长)路径长度,带边权值时可能求结最短(长)路径长度,还可能求解最小(大)边权值和,而有时边权值还可能为负。问题会很复杂,有四种解决方法:
一、Floyed-Warshall算法(O(n^3))
最简单最直接的算法,时间复杂度最高,但也是最万能的算法。能够处理出现负边权的情况,但不能处理存在负权回路(负环)的情况(即某个环的总权值是负值)。
方法就是通过建立另一个数组直接记录两点间的路径,逐步更新以得到最短路径。具体方法是遍历两点间的点k作为中点,然后向两侧扩展i、j,直到得到两点,最终的结果就是最短路径。
基本代码:(a[][]为求解的图,dis[][]为最短路径)
其中准备工作中需要将图a[][]中的边即直接相连的点初始化到dis[][]中。特殊题目中,可以通过设置无意义的值标记非直接相连的点,也可以新建立数组表示,还可以访问a[][]解决。
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j])
dis[i][j]=dis[i][j]||(dis[i][k]+dis[k][j])//判断两点是否相通
}
-
结果自然就是dis[x][z],(x,z是最短路径的起点和终点)。
二、Dijkstra算法(O(n^2))
用以计算从一个点到其它所有点的最短路径,一种单源最短路径算法。其实就是找到一个点,由这个点一步一步拓展路径求解,所以它的处理方法中会逐渐沿着多条通路计算路径长度,最终得到最短的到达终点路径。不过这个方法不能解决存在负边权值的情况。
具体方法:选定一个起点,一步一步寻找最短的路径所能到达的下一个点,记录当前能够到达的点和路径长度(或权值),然后更新所有剩余点可能的最短路径,并对每一个已经确定路径的点标记前一个顶点。直到找到题目要求的终点或者遍历完图,得到答案。应该建立两个数组,一个记录当前路径长度对应的顶点(或包含长度),另一个记录顶点是否被遍历,还可以建立一个记录前驱顶点的数组。
基本代码:(a[][]为求解的图,dis[]为最短路径长度,b[]记录顶点是否被遍历,pre[]为路径某结点的前驱结点)
准备工作,起点为x时将dis[x]和pre[x]赋值0,而终点z的dis[z]赋值无穷大数,b[]数组初始化0表示未遍历,,当前中点为y。
for(int o=1;o<n;o++)
{
for(int i=1;i<=n;i++)
{
if(!b[i]&&(dis[i]<minn))
{
minn=dis[i];
y=j;
}
}
if(y==0) break;
b[y]=1;
for(int i=1;i<=n;i++)
{
if(dis[i]+a[y][i]<dis[i])
{
dis[i]=dis[y]+a[y][i];
pre[i]=y;
}
}
}
-
其结果就是dis[z],若想找出最短路径,通过pre[z]寻找即可。
三、Bellman-Ford算法(O(nm)(n为顶点数,m为边数))
简称Ford算法。也是计算从一个点到其它所有点的最短路径,也是一种单源最短路径算法。不过,它不再是通过枚举点寻找最短路径,而是枚举边。它相较于Dijkstra算法的优点是能够处理存在负权值的情况,但仍不能处理存在负权回路(负环)的情况。
具体方法:它的实现和Dijkstra算法基本类似,只是中间稍有区别。枚举的符合条件的边自然是能够连接中间点和当前最短路径的点,而边的长度(或权值)会被记录,所以这种期情况下其实对应的图应该用数组模拟邻接表存储或者我所谓的模型表示法,即用结构体存储边的信息。
基本代码:(a[]表示求解的图,dis[]为最短路径,pre[]为前驱,x为起点,y为中间点,z为终点)
准备工作,将dis[x]和pre[x]赋值0,而终点z的dis[z]赋值无穷大数。l,r代表左右端点,len表示长度。
for(int o=1;o<n;o++)
{
for(int i=1;i<=m;i++)
{
if(dis[a[i].l]+a[i].len<dis[a[i].r])
{
dis[a[i].r]=dis[a[i].l]+a[i].len;
pre[a[i].r]=a[i].l;
}
}
}
-
其结果也是dis[z],也可通过pre[z]寻找找出最短路径。这样做的优点是不需要再单独找中点然后更新其它点了,而是直接更新最短路径。
四、SPFA算法(O(mk)(m是边数,k是个常数,平均值为2))
实则是Ford算法利用队列进行的优化,但SPFA的优点在于它可以判断负环。形式上确实和广搜很像,但区别是广搜只是用来遍历图的所有点或边,而不是寻找路径,所以不会出现同一个点还要遍历多次的情况,而寻找最短路径不同,它还有可能经过同一个点不止一次,区别在此。
具体方法:初始时将起点加入队列,之后是从队列取出一个元素,再修改所有其它不在队列里面的与该点相邻的点,修改后将对应的点放入队列,直到队列为空结束。而如果单个点进队或出队次数超过n-1次,说明存在负环,无法求得最短路径。
基本代码:(a[]表示求解的图,dis[]为最短路径,pre[]为前驱,b[]表示是否在队列中,aans暂时存储从队列中取出的中间点)
准备工作,b[]清零,dis[1]=0且dis[]的其它元素赋值无穷大。
q.push(x);
for(;q.size();)
{
aans=q.front();
q.pop();
b[aans]=0;
for(int i=1;i<=m;i++)
{
if(a[i].l==aans)
{
if(dis[a[i].l]+a[i].len<dis[a[i].r])
{
dis[a[i].r]=dis[a[i].l]+a[i].len;
pre[a[i].r]=a[i].l;
if(!b[a[i].r])
{
b[a[i].r]=1;
q.push(a[i].r);
}
}
}
}
}
结果自然也是dis[z],同样可以通过pre[z]寻找最短路径。至于要判断是否有负环,那只能再多一个数组判断了。