图的定义
图(graph)G = (V,E)由顶点(vertex)的集V和边(Edge)的集E组成。有时也把边称作弧(arc),如果点对(v,w)是有序的,那么图就叫做有向的图(有向图)。顶点v和w邻接(adjacent)当且仅当(v,w)属于E。
如果无向图中从每一个顶点到其他每个顶点都存在一条路径,则称该无向图是连通的(connected)。具有这样性质的有向图称为是强连通的(strongly connected)。如果有向图不是强连通的,但它的基础图(underlying graph)(也就是其弧上去掉方向说形成的图)是连通的,那么称该有向图是弱连通的(weakly connected)。完全图(complete graph)是其每一对顶点间都存在一条边的图。
如下表示了一个有着7个顶点和12条边的有向图。
拓扑排序
拓扑排序(topological sort)是对有向无环图的顶点的一种排序,它使得如果存在一条从vi到vj的路径,那么在排序中vj出现在vi的后面。正是由于这个特性,如果图含有回路,那么拓扑排序是不可能的。
求拓扑排序算法的一种简单方式:选中一个没有入边的顶点,显示出该点,并将它和它的边一起从图中删除,然后对图的其余部分应用同样的方法处理。
所谓入度(indegree)是指的顶点v的边(u,v)的条数。假设每一个顶点的入度被存储且图被读入一个邻接表中,下面的代码则可以生成一个拓扑排序。
对上图应用拓扑排序的结果如下:
最短路径算法
单源最短路径问题:给定一个加权图G=(V,E)和一个特定顶点s作为输入,找出从s到G中每一个其他点的最短加权路径。
如下图所示,从v1到v6的最短加权路径的值为6(v1-v4-v7-v6),从v2到v5的最短加权路径的值为5(v2-v4-v5)。
下面这个图从v5到v4的最短加权路径可就有意思了,它是1么?不是。按照v5-v4-v2-v5-v4的路径走则是一条更短的路径了,因为这是带负值回路的图。而由于带负值而引入的循环,这个循环叫做负值回路(negative-cost cycle),当它出现在图中时,最短路径问题就是不确定的了。有负值的边未必不好,但它们明显使问题更加难了。
当未指明所讨论的是加权路径还是无权路径时,如果图是加权的,那么路径就是加权的。
广度优先搜索
请先忽视下图中所有的下标,让我们从头开始。随意选择一个点,此处选择v3,作为切入点。因此到v3的距离为0。从v3出发,距离为1的结点是v1和v6;继续下一步,v6已经无路可走,而与v1距离为1的是v2和v4,因此对它们标记上2;继续下去,v2和v4走一步都可以到v5,v4走一步可以到v7,因此v5和v7被标记为3。至此搜索便结束了。
这就是广度优先搜索(breadth-first search),该方法按层处理顶点。距起始点最近的那些顶点首先被求值,最远点则最后被求值,这很像对树的层序遍历(level-order traversal)。
Dijkstra算法
前面的广度优先搜索中的图是无权图,而如果一旦变成了加权图,那么问题就变得困难起来了。
对于每个顶点,我们标记为known以及unknown,和上面一样,还得有一个距离的dv。与无权最短路径一样,Dijkstra算法也是按阶段进行,在每个阶段选择一个顶点v,它在所有unknown顶点中具有最小的dv,同时算法声明从s到v的最短路径是known的。然后紧接着,不断的进行下去即可。
那么这个算法到底是怎么回事了?请看下图。
图中已经对权重做好了标记,以v1作为切入点,因此初始情况如下左图。
v1此时已经是known的了,而其有2个邻接点v2和v4,因此可以调整为如下右图。正无穷图标标识没有连通。pv表示前一个邻接点。
毫无疑问这里会接下来走到v4去,因为v4的权重为1比v2的权重为2要小。调整为如下左图。
可能你已经看到了上图中的右图而好奇为什么下一步是v2,但是v4根本不能走到v2。因为v4能够走到的,比如v3,权重从v1开始一共是3,这比从v1到v2还要大。于是就跳转回到了v2。
下一步便走到了v5,因为只有值为3的权重,同样的v3也是,于是它们俩被双双标记为known。如下左图所示。
紧接着走到了v7,同时v6下调到了5+1=6得到了如下右图。至于为什么要做这个调整,是因为此时v1到v7的加权为1+4=5,而v7到v6的加权为1,所以就有了这个调整。
最后便顺势走到了v6完成了整个Dijkstra算法,它们都已被标记为known。
在后面还将会有一种斐波那契堆,针对Dijkstra算法做了优化,欢迎大家的继续关注。
具有负边值得图
而如果一个图具有负的边值,那么Dijkstra算法就行不通了。这是因为一个顶点u被声明为known后,那就可能从某个另外的unknown顶点v有一条回到u的负的路径。而“回到”就意味着循环,前面的例子中我们已经知道了循环是多么的……
问题并非没有解决的办法,如果我们有一个常数X,将其加到每一条边的值上,这样除去负的边,再计算新图的最短路径,最后把结果应用到原图上。然后这个解决方案也是布满了荆棘,因为居多许多条边的路径变得比那些具有很少边的路径权重更重了。如果我们将s放到队列中,然后再每一个阶段让一个顶点v出队,找出所有与v邻接的顶点w,使得dw>dv+cvw,然后更新到dw和pw,并在w不在队列中时将它放到队列中,可以为每一个顶点设置一个位(bit)以指示它在队列中出现的情况。
无环图
如果图是无环的,则可以通过改变声明顶点为known的顺序,或者叫做顶点选取法则来改进Dijkstra算法。这种方法通过拓扑排序来选择顶点,由于选择和更新可以在拓扑排序执行的过程中执行,因此新的算法只需要一趟就可以完成。
通过下面这个动作结点图(activity-node graph)来解释什么是关键路径分析(critical path analysis)再合适不过了。一条边(v,w)表示动作v必须在动作w开始前完成,如前面说描述的那样,这就意味着图必须是无环的。
为了进行这些运算,我们把动作结点图转化成事件结点图(event-node graph),每个事件对应于一个动作和所有与它相关的动作完成。
所以现在我们需要找出事件的最早完成时间,只要找出从第一个事件到最后一关事件的最长路径的长。因为有正值回路(positive-cost cycle)的存在最长路径问题常常是没有意义的。而由于事件结点图是无环图,那就不需要担心回路的问题了,这样一来就不用有所顾忌了。
以下是最早完成时间。
以下是最晚完成时间。
借助顶点的拓扑排序计算最早完成时间,而最晚完成时间则通过倒转拓扑排序来计算。
而事件结点图中每条边的松弛时间(slack time)代表对应动作可以被延迟而不推迟整体完成的时间量,最早完成时间、最晚完成时间和松弛时间如下所示。
某些动作的松弛时间为0,这些动作是关键性的动作,它们必须按计划结束。至少存在一条完成零-松弛边组成的路径,这样的路径是关键路径(critical path)。
感谢大家的支持,欢迎大家的关注或收藏。
为使本文得到斧正和提问,转载请注明出处: