图论---算法篇

图论中的算法基本都是提出后,经过检验的.我就不讨论算法很基础的原理,只是从看懂一个算法的角度去学习.本着不花时间去重复别人优秀工作的原则,本文中很多部分引用了别人的工作,甚至是照搬过来,因为我觉得算法这东西已经类似真理,证明不需要你,你可以看得懂,别人也可以,只是表达方式不同,别人有优秀的表达方式,我为什么不用呢!

算法类

戴克斯特拉算法(D.A)

描述: 又译迪杰斯特拉算法,使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。

原理: 迪杰斯特拉算法主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.

迪杰斯特拉算法算法图示

结合上图,详细解释迪杰斯特拉算法:
通过维护一个两个集合来实现:

1.一个集合内存储已经找到的最短距离及其路径(假设为D)
2.另一个是未找到的最短距离的点到起始顶点的路径(假设为U)
下面更新U的过程也是如此

图示目标:寻找顶点1到所有顶点的最短路径.

第一步:初始化D,U
初始节点到自身距离为0,直接初初始化到D集合,U集合维护顶点到初始节点1的距离和长度,根据上图,可初始化为以下两个集合.

enter description here

注: 图中表示节点名称,每个集合记录距离和路径,路径中的数字也表示节点名称,空间不够,不标单引号了.

以下步骤是不断更新两个集合的过程:

1.D根据U更新当前最短路径到自己(扩张过程)
2.U根据新加入的节点更新自己(松弛过程)
3.迭代以上过程,直到所有路径遍历完

下面是各个迭代过程图示:

第二步
由前面的初始情况可知,与节点1直接相连接一共3个点,最近的点是2.把1与2的当前最短距离和路径加到集合D中,然后根据新的集合D,更新集合U的信息,更新的过程是:

1.首先看看2节点的出边有那些,图中可以看到是:3,4
2.节点1经过2节点到达上面两个节点与直接到达上面两个节点的距离比较,那个距离小就把信息更新到U集合中


第三步

上一次迭代完成后,集合D有1,2两个点,这两个点能到达的点是6,3,4;广度遍历路径得到1到3个节点的3最近,将节点3填到D中,然后更新U.
此时的广度遍历就是从1开始,可以经过2,最终到达3,4,6其中一个节点的路径.具体包括:

a. 1->3
b. 1->6
c. 1->2->4

第四步
上一次迭代完成后,集合D有1,2,3三个点,这三个点能到达的点是6,4;广度遍历路径得到1->3->6这路径最短,将6更新到D后,更新U.


第五步
上一次迭代完成后,集合D有1,2,3,6四个点,这四个点能到达的点是5,4;广度遍历路径得到1->3->4这路径最短,将4更新到D后,更新U.


第六步
上一次迭代完成后,集合D有1,2,3,6,4五个点,这五个点能到达的点是5;广度遍历路径得到1->3->6->5这路径最短,将4更新到D后,更新U.

最终,节点1到各个节点的最短路径及其长度都保存在集合D中.

注:迪杰斯特拉算法也可用于有向图最短路径查找,还有一个迪杰斯特拉的优化算法双向的迪杰斯特拉,改进的地方是从源点和终点同时广度优先搜索最短路径,总的来说双向的迪杰斯特拉比一般迪杰斯特拉更快.


最短路径快速算法(SPFA)

描述:
SPFA算法是西南交通大学段凡丁于1994年发表的,是一个用于求解有向带权图单源最短路径的改良的贝尔曼-福特算法。这一算法被认为在随机的稀疏图上表现出色,并且极其适合带有负边权的图。 然而SPFA在最坏情况的时间复杂度与贝尔曼-福特算法相同,因此在非负边权的图中仍然最好使用戴克斯特拉算法。

原理:

最短路径快速算法图示

声明:
图片来源于:最快最好用的——spfa算法


弗洛伊德算法(Floyd-Warshall)

描述:
是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包.

原理:
假设我有以下graph:

演示graph

可以得到以下的邻接矩阵:

graph的邻接矩阵

该邻接矩阵D记录了有向图中,直接可达节点之间的距离.
下面就1->3的最短路径进行分析:

直接可达的路径权重为:9
比这短的路径可能是1->X->3 (X是除1,3外的所有点中的一个或多个)

假设X长度为1,X就是2,4,5其中一个值,1->X->3的路径长度小于9,在矩阵中就是D[1][x]+D[x][3]<9(为了理解,假设矩阵从1开始编号).
D[1][2]+D[2][3]=5+3=8<9 更新最短路径长度
D[1][4]+D[4][3]=∞+∞=∞
D[1][5]+D[5][3]=∞+∞=∞

假设X长度是2,X就是考虑顺序在2,4,5中任取两个.有以下情况
D[1][2]+D[2][4]+D[4][3]=5+2+∞=∞
D[1][4]+D[4][2]+D[2][3]=∞+∞+3=∞
…剩下组合省略

同理X长度为3时,有以下情况
D[1][2]+D[2][4]+D[4][5]+D[5][3]=2+2+1+2=7<8 更新
…剩下组合省略

最后找到,1到3的最短路径是,1->2->4->5->3,最短距离为8,更新D矩阵得到:

更新后D矩阵

在这个新的D上继续算其他节点之间的最短路径.当然,这在程序中就是简单遍历,其实这里X的长度就是1到3中间需要经过多少节点.所有节点遍历完成,得到graph内两两节点之间的最短距离.


克鲁斯卡尔算法(K.A)

描述:
一种用来查找最小生成树的算法,由Joseph Kruskal在1956年发表。用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪心算法的应用。和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。

原理:

克鲁斯卡尔算法算法图示

把所有边选出来,并按照权重进行排序,从权重最小边开始选择,每次选择一条边,每次选择的边不能与原来的边构成环,知道选择的边包含所有的节点.


普里姆算法(P.A)

描述:
图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

原理:
从单一顶点开始,普里姆算法按照以下步骤逐步扩大树中所含顶点的数目,直到遍及连通图的所有顶点。

输入:一个加权连通图,其中顶点集合为V,边集合为E;
初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {};
重复下列操作,直到Vnew = V:
    在集合E中选取权值最小的边(u, v),其中u为集合Vnew中的元素,而v则是V中没有加入Vnew的顶点(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
    将v加入集合Vnew中,将(u, v)加入集合Enew中;
输出:使用集合Vnew和Enew来描述所得到的最小生成树。

原图

1
(1)顶点D被任意选为起始点。顶点A、B、E和F通过单条边与D相连。A是距离D最近的顶点,因此将A及对应边AD以高亮表示。


2
(2)下一个顶点为距离D或A最近的顶点。B距D为9,距A为7,E为15,F为6。因此,F距D或A最近,因此将顶点F与相应边DF以高亮表示。


3
(3)算法继续重复上面的步骤。距离A为7的顶点B被高亮表示。


4
(4)在当前情况下,可以在C、E与G间进行选择。C距B为8,E距B为7,G距F为11。E最近,因此将顶点E与相应边BE高亮表示。


5
(5)这里,可供选择的顶点只有C和G。C距E为5,G距E为9,故选取C,并与边EC一同高亮表示。


6
(6)顶点G是唯一剩下的顶点,它距F为11,距E为9,E最近,故高亮表示G及相应边EG。


7
(7)现在,所有顶点均已被选取,图中绿色部分即为连通图的最小生成树。在此例中,最小生成树的权值之和为39。


拓扑排序算法(TSA)

描述:
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。

图形的顶点可以表示要执行的任务,并且边缘可以表示一个任务必须在另一个任务之前执行的约束; 在这个应用中,拓扑排序只是一个有效的任务顺序。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网

一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行.在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。

原理:
重复以下两个步骤,即可以得到拓扑序列.

1.在有向图中任意选择一个无前驱的节点,并且作为当前的拓扑序列输出
2.删除与 前面选择的无前驱节点 的所有关联的边

例子:

e拓扑排序算法例子

下面是得到其中一个拓扑序列的过程:

第一步:a是无前驱的节点,选择
第二步:a去掉后,b,c为无前驱节点,任选一个,假设选择c
第三步:a,c去掉,只有b为无前驱节点,选择
第四步:a,c,b去掉后,d,e为无前驱节点,任选一个,假设选择d
第五步:a,c,b,d去掉后,e,f为无前驱节点,任选一个,假设选择f
第六步:a,c,b,d,f去掉后,只有e为无前驱节点,选择
第六步:a,c,b,d,f,e去掉后,只有g为无前驱节点,选择
第七步:所有节点遍历完成,得到拓扑序列

最终得到的拓扑序列为a->c->b->d->f->e->g


关键路径算法(CPA)

描述:
关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径,一个AOE网中不一定只有一条关键路径,可能会有多条。
关键活动:关键路径上的活动(边)。
由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。关键路径长度是整个工程所需的最短工期。

原理:
现有无向有权图

无向有权图

根据这个graph定义四个值:前两个针对顶点,后两个针对边

(1)事件最早开始时间:顶点最早发生的时间。
节点B只有A指向,所以其最早开始时间是1,对于节点E,需要等待B,C,D完成才能开始,缺一不可,所以其最早时间是A->D->E:8

(2)事件最晚开始时间:顶点最晚发生的时间,超出则会延误整个工期。
从右往左推,假设求得关键路径是A->D->E->H->J:18,以H为例,J->H:18-7=11表示J最晚开始时间,超过这个时间J无法在18内完成.J->I->H为例,18-5-1=12,也是H的最迟开始时间,但是11比12靠前,所以11为H的最迟开始时间.

(3)活动的最早开始时间:边最早发生时间。
求某一点的最早开始时间就是计算该节点前所有几点完成的时刻.比如E开始时,必须等待A->C->E(4);A->B->E(4);A->D->E(8)上的边必须先完成,取最长时间8为E的最早开始时间,比如上图各节点的最早开始时间如下:

最早发生时间

图中所标数字是每个节点的最早开始时间,假设源点(A)从0开始算起,汇点不需要时间处理.那么整个网络,最长的路径就是18(A->D->E->H->J).各个点的最迟发生时间从后往前推,取较小的,比如H这个点J->H(18-7=11),J->I->H(18-5-1=12),就取11.

(4)活动的最晚开始时间:边最晚发生时间。不推迟工期的最晚开工时间。

以上数字均是时间点,从源点算起.
假设起点是vo,则我们称从v0到vi的最长路径的长度为vi的最早发生时间,同时,vi的最早发生时间也是所有以vi为尾的弧所表示的活动的最早开始时间,使用e(i)表示活动ai最早发生时间,除此之外,我们还定义了一个活动最迟发生时间,使用l(i)表示,不推迟工期的最晚开工时间。我们把e(i)=l(i)的活动ai称为关键活动.

下面是算法的推理过程:
1.构建graph网络
2.从源点开始求graph的拓扑排序序列,如果序列个数小于graph节点个数,则graph存储环,程序退出,否则根据拓扑序列求各个节点的最早开始时间ve(j),计算公式为:

1
2
3
$$
ve(j)=Max(ve(i)+Dur(i,j))
$$

3.从汇点求graph的逆拓扑序列,然后根据该序列求各个节点的最晚开始时间vl(j),计算公式为:

1
2
3
$$
vl(j)=Min(vl(i)-Dur(i,j))
$$

4.如果ve(j)=vl(j),则i-.j是关键路径,否则不是.


广度优先搜索算法(BFS)

描述:
又译作宽度优先搜索,或横向优先搜索,是一种图形搜索算法。简单的说,BFS是从根节点开始,沿着树的宽度遍历树的节点。如果所有节点均被访问,则算法中止。广度优先搜索的实现一般采用open-closed表。

原理:
优先搜索同以层次的节点,和优先往纵深搜索不同,过程如下图,代码过程不描述.

广度优先搜索算法


深度优先搜索算法(DFS)

描述:
是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。

原理:
过程如下图,代码过程不描述.

深度优先搜索算法

参考:
维基百科-图论
百度百科-迪杰斯特拉算法
最短路径问题—Dijkstra算法详解
数据结构—拓扑排序详解
傻子也能看懂的弗洛伊德算法
算法学习记录-图——应用之关键路径
数据结构----关键路径详解