乘汽车旅行的人总希望找出到目的地的尽可能的短的行程。如果有一张地图并在图上标出每对十字路口之间的距离,如何找出这一最短行程?
一种可能的方法就是枚举出所有路径,并计算出每条路径的长度,然后选择最短的一条。那么我们很容易看到,即使不考虑包含回路的路径,依然存在数以百万计的行车路线,而其中绝大多数是不值得考虑的。
在这一章中,我们将阐明如何有效地解决这类问题。在最短路径问题中,给出的是一有向加权图G=(V,E,W),其中V为顶点集,E为有向边集,W为边上的权集。最短路径问题研究的问题主要有:单源最短路径问题、与所有顶点对之间的最短路径问题。
一、 单源最短路径问题
所谓单源最短路径问题是指:已知图G=(V,E),我们希望找出从某给定的源结点S∈V到V中的每个结点的最短路径。
首先,我们可以发现有这样一个事实:如果P是G中从vs到vj的最短路,vi是P中的一个点,那么,从vs沿P到vi的路是从vs到vi的最短路。
(一)Dijkstra算法
对于图G,如果所有Wij≥0的情形下,目前公认的最好的方法是由Dijkstra于1959年提出来的。
例1 已知如下图所示的单行线交通网,每弧旁的数字表示通过这条单行线所需要的费用,现在某人要从v1出发,通过这个交通网到v8去,求使总费用最小的旅行路线。
500){this.resized=true;this.style.width=500;}">
Dijkstra方法的基本思想是从vs出发,逐步地向外探寻最短路。执行过程中,与每个点对应,记录下一个数(称为这个点的标号),它或者表示从vs到该点的最短路的权(称为P标号)、或者是从vs到该点的最短路的权的上界(称为T标号),方法的每一步是去修改T标号,并且把某一个具T标号的改变为具P标号的点,从而使G中具P标号的顶点数多一个,这样至多经过n-1(n为图G的顶点数)步,就可以求出从vs到各点的最短路。
在叙述Dijkstra方法的具体步骤之前,以例1为例说明一下这个方法的基本思想。例1中,s=1。因为所有Wij≥0,故有d(v1, v1)=0。这时,v1是具P标号的点。现在考察从v1发出的三条弧,(v1, v2), (v1, v3)和(v1, v4)。如果某人从v1出发沿(v1, v2)到达v2,这时需要d(v1, v1)+w12=6单位的费用;如果他从v1出发沿(v1, v3)到达v3,这时需要d(v1, v1)+w13=3单位的费用;类似地,若沿(v1, v4)到达v4,这时需要d(v1, v1)+w14=1单位的费用。因为min{ d(v1, v1)+w12,d(v1, v1)+w13,d(v1, v1)+w14}= d(v1, v1)+w14=1,可以断言,他从v1到v4所需要的最小费用必定是1单位,即从v1到v4的最短路是(v1, v4),d(v1, v4)=1。这是因为从v1到v4的任一条路P,如果不是(v1, v4),则必是先从v1沿(v1, v2)到达v2,或者沿(v1, v3)到达v3。但如上所说,这时他已需要6单位或3单位的费用 ,不管他如何再从v2或从v3到达v4,所需要的总费用都不会比1小(因为所有wij≥0)。因而推知d(v1, v4)=1,这样就可以使v4变成具P标号的点。现在考察从v1及v4指向其余点的弧,由上已知,从v1出发,分别沿(v1, v2)、(v1, v3)到达v2, v3,需要的费用分别为6与3,而从v4出发沿(v4, v6)到达v6所需的费用是d(v1, v4)+w46=1+10=11单位。因min{ d(v1, v1)+w12,d(v1, v1)+w13,d(v1, v4)+w46}= d(v1, v1)+w13=3。基于同样的理由可以断言,从v1到v3的最短路是(v1, v3),d(v1, v3)=3。这样又可以使点v3变成具P标号的点,如此重复这个过程,可以求出从v1到任一点的最短路。
在下述的Dijstra方法具体步骤中,用P,T分别表示某个点的P标号、T标号,si表示第i步时,具P标号点的集合。为了在求出从vs到各点的距离的同时,也求出从Vs到各点的最短路,给每个点v以一个λ值,算法终止时λ(v)=m,表示在Vs到v的最短路上,v的前一个点是Vm;如果λ(v)=∞,表示图G中不含从Vs到v的路;λ(Vs)=0。
Dijstra方法的具体步骤:
{初始化}
i=0
S0={Vs},P(Vs)=0 λ(Vs)=0
对每一个v<>Vs,令T(v)=+ ∞,λ(v)=+ ∞,
k=s
{开始}
①如果Si=V,算法终止,这时,每个v∈Si,d(Vs,v)=P(v);否则转入②
②考察每个使(Vk,vj)∈E且vj Si的点vj。
如果T(vj)>P(vk)+wkj,则把T(vj)修改为P(vk)+wkj,把λ(vj)修改为k。
③令500){this.resized=true;this.style.width=500;}" align=middle>
如果500){this.resized=true;this.style.width=500;}" align=middle>,则把500){this.resized=true;this.style.width=500;}" align=middle>的标号变为P标号500){this.resized=true;this.style.width=500;}" align=middle>,令500){this.resized=true;this.style.width=500;}" align=middle> ,k=ji,i=i+1,转①,否则终止,这时对每一个v∈Si,d(vs,v)=P(v),而对每一个500){this.resized=true;this.style.width=500;}" align=middle> 。
算法实现:
1、 数据结构
* 用邻接矩阵表示图G
* 用一维数组D[I]存放从源点到I点的最短路径长度或其上界。(上面算法中的P标号与T标号实际上存放着源点到某点的最短路径长度或其上界,因此我们可以统一用D数组来表示)。
* 用一维数组P,其中P[I]记录到I点的最短路径中前一个顶点的序号。
{$R+}
2、源程序
500){this.resized=true;this.style.width=500;}" resized="true">
500){this.resized=true;this.style.width=500;}">
500){this.resized=true;this.style.width=500;}" resized="true">
500){this.resized=true;this.style.width=500;}">
500){this.resized=true;this.style.width=500;}">
(二)Bellman-Ford算法
在单源最短路径问题的某些实例中,可能存在权为负的边。如果图G=(V,E)不包含从源s可达的负权回路,则对所有v∈V,最短路径的权定义d(s,v)依然正确,即使它是一个负值也是如此。但如果存在一从s可达的负回路,最短路径的权的定义就不能成立。S到该回路上的结点就不存在最短路径。当有向图中出现负权时,则Dijkstra算法失效。当不存在源s可达的负回路时,我们可用Bellman-Ford算法实现。
下面我们介绍有向图中,存在具有负权的弧时,求最短路的方法。
为了方便起见,不妨设从任一点vi到任一点vj都有一条弧(如果在Gk ,(vi,vj)不存在,则添加(vi,vj)且令wij=+∝)。
显然,从vs到vj的最短路总是从vs出发,沿着一条路到某个点vi,再沿(vi,vj)到vj的(这里vi可以是vs本身),由本章开始时介绍的一个结论可知,从vs到vi的这条路必定是从vs到vi的最短路,所以d(vs,vi)必满足如下方程:
500){this.resized=true;this.style.width=500;}" align=middle>
为了求得这个方程的解500){this.resized=true;this.style.width=500;}" align=middle> (这里P为图G中的顶点数目),可用如下递推公式:
开始时,令
500){this.resized=true;this.style.width=500;}" align=middle>
对t=2,3,...,
500){this.resized=true;this.style.width=500;}" align=middle>
若进行到某一步,例如第k步时,对所有j=1,2,...,p,有:
500){this.resized=true;this.style.width=500;}" align=middle>
则500){this.resized=true;this.style.width=500;}" align=middle> 即为vs到各点的最短路的权。
不难证明:
(1)如果G是不含回路的赋权有向图,那么,从vs到任一个点的最短路必可取为初等路,从而最多包含P-2个中间点;
(2)上述递推公式中的 500){this.resized=true;this.style.width=500;}" align=middle>是在至多包含t-1个中间点的限制条件下,从vs到vj的最短路的权。
由(1)(2)可知:当G中不含负回路时,上述算法最多经过p-1次迭代必定收敛,即对所有的j=1,2,...,P,均有500){this.resized=true;this.style.width=500;}" align=middle> ,从而求出从vs到各个顶点的最短路的权。
如果经过p-1次迭代,存在某个j,使500){this.resized=true;this.style.width=500;}" align=middle> ,则说明G中包含有负回路。显然,这时从vs到vj的路是没有下界的。
根据以上分析,Bellman-Ford算法可描述为:
500){this.resized=true;this.style.width=500;}" align=middle resized="true">
算法实现
1、 数据结构(同Dijkstra算法,略)
2、源程序
500){this.resized=true;this.style.width=500;}" align=middle>
500){this.resized=true;this.style.width=500;}" align=middle resized="true">
500){this.resized=true;this.style.width=500;}" align=middle>
500){this.resized=true;this.style.width=500;}" align=middle>
(三)有向无回路图中的单源最短路径
若图G是一个无回路有向图,求图G的单源最短路径问题可以在O(V+E)时间内计算出单源最短路径。在有向无回路图中最短路径总是存在的,这是因为即使图中有权为负的边,也不可能存在权为负的回路。
算法开始时先对有向无回路图进行拓朴排序,以便获得结点的线性序列。如果从结点u到结点v存在一通路,则在拓扑序列中u先于v。在拓扑排序过程中我们仅对结点执行一趟操作。当对每个结点进行处理时,从该结点出发的所有边也被松驰。
算法描述如下:
500){this.resized=true;this.style.width=500;}" align=middle>
二、每对结点间的最短路径
我们要讨论找出图中每对结点间最短路径问题。这个问题在实践中常常会出现。例如,对一公路图,要造表说明其上的每对城市间的距离时就可能出现这种问题。
对于有向图G(V,E,W),要求每对结点间的最短路径,我们可以把单源最短路径算法运行|V|次来解决,每次依序把图中的每个结点作为源点。如果所有边的权为非负,可以采用Dijkstra算法,算法的运行时间为O(V3);如果允许有负权边的存在,我们必须对每个结点运行一次速度较慢的Bellman-Ford算法,其中运行时间为O(V2E),对稠密图则为O(V4)。
下面我们介绍一种动态程序设计方案来解决可以存在负权边但无负回路的有向图G=(V,E),每对结点间的最短路径问题,所产生的算法称为Floyd-Warshall算法,其运行时间为O(V3)。
(一)最短路径的结构
在Floyd-Warshall算法中,考察的是一条最短路径上的"中间"结点,其中某条简单路径P=<V1,V2,...,Vj>的中间结点是P中除V1和Vj以外的任何结点,即任何属于集合{V2,V3...,Vj-1}的结点。
该算法主要基于下列观察。设G的结点为V={1,2,...,n},并对某个k考察其结点子集{1,2,...,k}。对任意一对结点i,j∈V,考察从i到j且其中间结点皆属于集合{1,2,...,k}的所有路径,设P是其中一条最小权路径(因为我们假定G中不包含负权回路,所以P是简单路径)。Floyd-Warshall算法利用了路径P与从i到j间的最短路径(所有中间结点皆属于集合{1,2,...,k-1}之间的联系。这一联系取决于k是否是路径p的一个中间结点。
如果k是路径p的中间结点,由如下图所示,我们把p分解为p1(i500){this.resized=true;this.style.width=500;}" align=absMiddle>k),p2(k500){this.resized=true;this.style.width=500;}" align=absMiddle>j)。由前面可知,p1是从i到k的一条最短路径,且其所有中间结构均属于集合{1,2,...,k}。
500){this.resized=true;this.style.width=500;}" align=absMiddle>
事实上,结点k不是路径p1的中间结点,所以p1是从i到k的一条最短路径,且满足所有中间结点均属于{1,2,...,k-1}。类似地,p2是从k到j的一条最短路径,且其中间结点皆属于集合{1,2,...,k-1}。
(二)解决每对结点间最短路径问题的一种递归方案
基于上述观察,我们将给出定义最短路径估计的一个递归公式。设500){this.resized=true;this.style.width=500;}" align=absMiddle> 为从结点i到结点j且满足所有中间均属于集合{1,2,...,k}的一条最短路径的权。当k=0时,从结点i到结点j的路径中根本不存在中间结点,因此它至多包含一条边,则有500){this.resized=true;this.style.width=500;}" align=absMiddle> ,递归定义由下式给出:
500){this.resized=true;this.style.width=500;}" align=absMiddle>
矩阵给出了最后解,对所有的i,j∈V成立――因为其所有中间点皆属于{1,2,...,n}。500){this.resized=true;this.style.width=500;}" align=absMiddle>
(三)自底向上计算最短路径的权
基于以上给出的递归定义,我们可以运用下面自底向上过程按k值的递增顺序计算500){this.resized=true;this.style.width=500;}" align=absMiddle> 。过程的输入是n*n的矩阵W。其返回值关于最短路径的权的矩阵。
500){this.resized=true;this.style.width=500;}">
(四) 建立最短路径
有时除了需要计算出一个带权的有向图中从任一顶点到其他顶点之间的最短路径的长度外,还要确定相应的最短路径。为此,可以设置一个n*n的矩阵P,当k是在Floyd算法中,使Dij达到最小值时,就置P[i,j]=k。约定P[i,j]=0,表示从顶点i顶点j的最短路径就是从i到j的边。下面是修改后的Floyd算法,其中包含了计算矩阵矩阵P的步骤。
500){this.resized=true;this.style.width=500;}" resized="true">
Var
Begin
K:=P[i,j];
If k=0 then return;
Path(i,k);
Print(k);
Path(k,j);
End;
(五)源程序
500){this.resized=true;this.style.width=500;}" resized="true">
500){this.resized=true;this.style.width=500;}">
500){this.resized=true;this.style.width=500;}">三、应用举例
1、设备更新问题。某企业使用一台设备,在每年年初,企业领导部门就要决定是购置新的,还是继续使用旧的。若购置新设备,就要支付一定的购置费用;若继续使用旧设备,则需支付一定的维修费用。现在的问题是如何制定一个几年之内的设备更新计划,使得总的支付费用最少。
例如,我们一个五年之内要更新某种设备的计划,若已知该种设备在各年年初的价格为:
第一年 第二年 第三年 第四年 第五年 11 11 12 12 13还已知使用不同时间(年)的设备所需要的维修费用为:
使用年数 0-1 1-2 2-3 3-4 4-5 维修费用 5 6 8 11 18可供选择的设备更新方案显然很多的,例如,每年都购置一台新设备,则其购置费用为11+11+12+12+13=59,而每年支付的维修费用为5,五年合计为25,于是五年总的支付费用为59+25=84。
双如决定在第一、三、五年各购进一台,这个方案的设备购置费为11+12+13=36,维修费为5+6+5+6+5=27。五年总的支付费用为63。
这个例子中一种最佳方案为在第1年、第3年各购置一台新设备,五年总费用为53。
编写一个程序,输入n年年初设备的价格与使用不同时间(年)的设备所需要的维修费用,为该企业领导部门确定一个方案使得在n年内为这台机器支付的总费用最少。
2、工程安排
一项工程由多道工序组成, 按照施工过程的要求,这些工序之间,客观上有一个必须遵守的先后关系。 对那些紧接在已知工序前的工序叫紧前工序,把在已知工序后边紧接的工序叫后项工序, 只有已知工序的所有紧前工序都完成,已知工序才能开始施工。例如某工程的工序表如下:
序代号 紧前工序 完成时间 序代号 紧前工序 完成时间 A - 6 F C 2 B - 2 G D 3 C A 3 H B,E 4 D A 5 I H 2 E A 3 J F,G,I 2一天中可以同时进行若干道工序。
编程要求:
求工程最少在几天内完成,并找出一种工程施工安排方案。
输入数据
输入文件名由键盘输入,该文件
第一行为总工序数N;
第二行至第N+1行为工序表的内容(依次是工序编号1到N的工序代号、 紧前工序、工序完成时间。
输出数据
输出文件名为OUTPUT.DAT,该文件有K+1行;
第一行为工程施工最短天数K
第二行至第K+1行为每一天施工的工序号
参考文件
参考输入文件example.txt(上表)
参考输出文件answer.txt
500){this.resized=true;this.style.width=500;}">