图论算法
图论算法在计算机科学中扮演着很重要的角色,它提供了对很多问题都有效的一种简单而系统的建模方式。很多问题都可以转化为图论问题,然后用图论的基本算法加以解决。这类问题算法主要包括Dijkstra,Floyd,Prim,最短路、网络流、二分图等算法。
背景
哥尼斯堡(KOnigsberg)七桥问题
在哥尼斯堡的一个公园里,有七座桥将河中两个岛及岛与河岸连接起来,问是否可能从这四块陆地中任一块出发,恰好通过每座桥一次,再回到起点。
1736年,在经过一年的研究之后,29岁的欧拉提交了《哥尼斯堡七桥》的论文,圆满解决了这一问题,同时开创了数学新一分支---图论。
七桥问题在论文中,欧拉将七桥问题抽象出来,把每一块陆地考虑成一个点,连接两块陆地的桥以线表示。并由此得到了如图一样的几何图形。 若我们分别用A、B、C、D四个点表示为哥尼斯堡的四个区域。这样著名的“七桥问题”便转化为是否能够用一笔不重复地画出过此七条线的问题了。若可以画出来,则图形中必有终点和起点,并且起点和终点应该是同一点,由于对称性可知由B或C为起点得到的效果是一样的,若假设以A为起点和终点,则必有一离开线和对应的进入线,若我们定义进入A的线的条数为入度,离开线的条数为出度,与A有关的线的条数为A的度,则A的出度和入度是相等的,即A的度应该为偶数。即要使得从A出发有解则A的度数应该为偶数,而实际上A的度数是5为奇数,于是可知从A出发是无解的。同时若从B或D出发,由于B、D的度数分别是3、3,都是奇数,即以之为起点都是无解的。由上述理由可知,对于所抽象出的数学问题是无解的,即“七桥问题”也是无解的。
由此我们得到:欧拉回路关系
(对于一个连通图,通常把从某结点出发一笔画成所经过的路线叫做欧拉路。人们又通常把一笔画成回到出发点的欧拉路叫做欧拉回路。具有欧拉回路的图叫做欧拉图。)
可知要使得一个图形可以一笔画,必须满足如下两个条件:
1. 图形必须是连通的。
2. 图中的“奇点”个数是0或2。
引进概念
图:
二元组G(V,E)称为图(graph)。V为顶点(vertex) 或结点(node)的集。E为图中边的集合。
•点用数字0...n-1表示
•点对(u,v)称为边(edge)或称弧
•子图(subgraph):边的子集和相关联的点集
•带权图:可以给边加权(weight),成为带权图,或加权图(weightedgraph).权通常代表费用、距离等,可以是正数,也可以是负数
•有向图:边都是单向(unidirectional)的,因此边(u,v)是有序数对.(最基本的图通常被定义为“无向图”,与之对应的则被称为“有向图”。两者的区别在于,有向图中的边是有方向性的。下图即是一个有向图,边的方向分别是:(1->2), (1-> 3), (3-> 1), (1->5), (2->3), (3->4), (3->5), (4->5), (1->6), (4->6)。
注意,图中的边(1->3)和(3->1)是不同的。有向图和无向图的许多原理和算法是相通的。)
有时用弧(arc)专指有向边
有时用弧(arc)专指有向边
•一条路径(path)是一个结点序列,路上的相邻结点在图上是邻接的
•如果结点和边都不重复出现,则称为简单路径(simplepath).如果除了起点和终点相同外没有重复顶点和边,称为圈(cycle).
•不相交路(disjoint path)表示没有除了起点和终点以外的公共点的路.更严格的全图:N个顶点的图,有N(N-1)/2个节点于(u,v),若邻接则改为非邻接,若非邻接则改为邻接,得到的图原图的补图
生成树:
•树:N个点,N-1条边的连通图(无环连通图)
•生成树:包含某图G所有点的树
•一个图G是树当且仅当以下任意一个条件成立
G有V-1条边,无圈
G有V-1条边,连通
任意两点只有唯一的简单路径
G连通,但任意删除一条边后不连通
最短路问题
对一幅图G,我们对每一条边赋权w(e),成为一个赋权图。H是G的一个子图,则W(H) = sigma(w(e)),也就是对每条边的权求和。寻找从一个点a到另一个b的一个子图,使得权和最小,即为最短路问题。
解法一:Dijkstra算法(仅适用于无负权边的情况)
把顶点集合V分成两组:•(1)S:已求出的顶点的集合(初始时只含有源点VO)•(2)V-S=T:尚未确定的顶点集合
将T中顶点按递增的次序加入到S中,保证:
•(1)从源点VO到S中其他各顶点的长度都不大于从VO到T中任何顶点的最短路径长度
•(2)每个顶点对应一个距离值
•S中顶点距离:从VO到此顶点的最短距离
•T中顶点距离:从V0到此顶点的只包括S中顶点作中间顶点的
算法图解:
不断求一个点集合中的每个点,和与他相邻点最短路的最小值。我们还是从实例出发,更容易讲解。求下面这个图从A到L的最短路。
(1)
令a1 = A(便于标记),t(a1) = 0(表示点a1到a的最短路),S={a1}(被选择的点的集合),T = 0(空集 表示被选择的最短路的边集)。
(2)
求与S中的点a1与他相邻的点的距离d,取点a2 = C使得距离最小。令S={a1,a2},T={AC}。
(3)
重复第二步,求S中的点a1,a2的相邻点中(去除已选择点),距离最小的那个,则为AB,CE。再取AB,CE中权和最小的一个,B,所以a3=B,令S={a1,a2,a3},T={AC,AB}。
…
持续下去,不断寻找,S集合中的每个点与他相邻点的最小距离,然后在这些最小距离中找到最小的那个加入到S中,同时加入相应的边。
直到到达终点L为止。最后得到的最短路为:
此算法是多项式时间可求解的。
解法二:Floyd算法(插点法)
Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题。
算法思想原理:
Floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径。从动态规划的角度看问题,我们需要为这个目标重新做一个诠释。从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,我们假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
算法描述:
暑假,小哼准备去一些城市旅游。有些城市之间有公路,有些城市之间则没有,如左图。为了节省经费以及方便计划旅程,小哼希望在出发之前知道任意两个城市之前的最短路程。
上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。我们可以用一个4*4的矩阵(二维数组e)来存储。比如1号城市到2号城市的路程为2,则设e[1][2]的值为2。2号城市无法到达4号城市,则设置e[2][4]的值为∞。另外此处约定一个城市自己到自己的路程也是0,例如e[1][1]为0。
如果求任意两点之间的最短路径,两点之间可以直接到达但却不是最短的路径,要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。每个顶点都有可能使得另外两个顶点之间的路程变短。
当任意两点之间不允许经过第三个点时,这些城市之间最短路程就是初始路程。
在只允许经过1号顶点的情况下,任意两点之间的最短路程更新为:
在只允许经过1号顶点的情况下,只需判断e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示的是从i号顶点到j号顶点之间的路程。e[i][1]+e[1][j]表示的是从i号顶点先到1号顶点,再从1号顶点到j号顶点的路程之和。其中i是1~n循环,j也是1~n循环接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。
我们需要在只允许经过1号顶点时任意两点的最短路程的结果下,再判断如果经过2号顶点是否可以使得i号顶点到j号顶点之间的路程变得更短。即判断e[i][2]+e[2][j]是否比e[i][j]要小,在只允许经过1和2号顶点的情况下,任意两点之间的最短路程更新为:
判断e[i][2]+e[2][j]是否比e[i][j]要小。同理,继续在只允许经过1、2和3号顶点进行中转的情况下,求任意两点之间的最短路程。任意两点之间的最短路程更新为:
最后允许通过所有顶点作为中转,任意两点之间最终的最短路程为:
最小生成树问题
同样在一个连通赋权图中,寻找一颗生成树使得权和最小。
Kruskal算法(避圈法):
(1)在G中选取最小权边e,并置边数i=1;
(2)当i=n-1时结束,否则转向(3);
(3设已选择的边为e,e2,.,e,在G中选不同于e,e2,.,e的边e;+1,使得e;+是满足与e,e,...,e不构成回路且权值最小的边
(4)置为i+1,转向(2)。
不断寻找最小权的边即可。
比如,寻找下图的最小生成树
结果如下
网络流问题
如下图所示,有一个顶点,称为源点(source);还有一个顶点,称为汇点(sink)。对于顶点,它最大流出2,因此它的最大流入为2,如下右图所示。而他的最大流也就是5。
要想计算最大流,同样可是使用前面的思想——分阶段进行。令开始时所有边都没有流,如下中间图所示。我们可以用残余图(residual graph)来表示对于每条边还能再添加上多少流。对于每一条边,可以从容量中减去当前的流而计算出残留的流。
第一步:假设我们选择路径,此时会发出2个单位的流通过这条路径的每一边,如下中间图所示。对比左图,我们做如下约定:一旦注满(使饱和)一条边,例如到和到,就将这条边从残余图(也就是中间图)去掉,如下右图所示。
第二步:接下来选择路径,此时也会发出2个单位的流通过这条路径的每一边,如下中间图所示。同样将残余图更新如下右图所示。
第三步:从上图的残余图中我们已经可以看出来最后一步的唯一一种走法了。
做如下图所示更新。
很显然无法走到,因此算法至此便终止了。因此正好5个单位的流是最大值。
如果一开始我们选择了如下图中间所示的走法,那么算法就会失败了,因为路已经被堵死了。
为了使算法得以成功运作,那么就要让流图中具有以相反方向发送流的路径,如下所示。那么对于如下右图中的残余图而言,从d返回到a的便成了3而非4,这是因为从t流到d的流量是3个单位。现在在残余图中就有a和d之间有2个方向,或者还有1个单位的流可以从导向,或者是3个单位的流导向相反的反向,当然,我们也可以撤销流。
紧接着如果通过到导入2个单位的流,算法就会从边取走2个单位的流,更新流图如下。
参考:
1.《数据结构》、《离散数学》、《算法导论》、《挑战程序设计竞赛》等书籍和必应、维基百科等网络。
2.https://blog.csdn.net/u012469987/article/details/51319574
3.https://www.cnblogs.com/hxsyl/p/3270401.html
4.https://wk.baidu.com/view/343fcfe8172ded630b1cb6f9
5.https://wk.baidu.com/view/be1b0a7f58cfa1c7aa00b52acfc789eb172d9e07