数据结构与算法——31. 图的应用:拓扑排序、强连通分支、最短路径问题、最小生成树

一、拓扑排序(Topological Sort)

很多问题都可转化为图, 利用图算法解决,例如制作煎饼的过程,以动作为顶点,以先后次序为有向边。

在这里插入图片描述

但问题是如果一个人独自做,所有动作的先后次序是什么?从加料开始?还是从加热烤盘开始?

从工作流程图得到工作次序排列的算法,称为“拓扑排序”。拓扑排序处理一个DAG(有向无圈图),输出顶点的线性序列。使得两个顶点v,w,如果G中有(v,w)边,即从顶点v到顶点w,那么在线性序列中v就出现在w之前。比如,在上图中“pour 1/4 cup”在“turn when bubbly之前”。

拓扑排序广泛应用在依赖事件的排期上,还可以用在项目管理、数据库/查询优化和矩阵乘法的次序优化上……

实现思路

拓扑排序可以采用DFS很好地实现:

  1. 将工作流程建立为图,工作项是节点,依赖关系是有向边
  2. 工作流程图一定是个DAG图,否则有循环依赖;
  3. 对DAG图调用DFS算法,以得到每个顶点的“结束时间”;
  4. 按照每个顶点的“结束时间”从大到小排序;
  5. 输出这个次序下的顶点列表。

制作煎饼的步骤顺序(其中一种)

在这里插入图片描述

二、强连通分支

我们先看一张非常巨大的图:由网页通过超链接连接而形成的图。以网页(URI作为id)为顶点,网页内包含的超链接作为边,可以转换为一个有向图。

在这里插入图片描述

美国路德学院计算机系网站链接情况,有三个有趣的现象:

  • 图中包含了许多路德学院其它系的网站;
  • 包含了一些爱荷华其它大学学院的网站;
  • 还包含了一些人文学院的网站。

我们可以猜想,Web的底层结构可能存在某些同类网站的聚集在图中发现高度聚集节点群的算法,即寻找“强连通分支(Strongly Connected Components)”算法

强连通分支,定义为图G的一个子集C,C中的任意两个顶点v,w之间都有路径来回,即(v,w)(w,v)都是C的路径,而且C是具有这种性质的最大子集

下图是具有3个强连通分支的9顶点有向图,一旦找到强连通分支,可以据此对图的顶点进行分类,并对图进行化简。

在这里插入图片描述

1. 转置的概念

在用深度优先搜索来发现强连通分支之前,先熟悉一个概念:Transposition转置。

一个有向图G的转置 G T \text G^ \text T GT,定义为将图G的所有边的顶点交换次序,如将(v,w)转换为(w,v),可以观察到图和转置图在强连通分支的数量和划分上,是相同的。比如下图:

在这里插入图片描述

2. 强连通分支算法:Kosaraju算法思路

  1. 首先,对图G调用DFS算法,为每个顶点计算“结束时间”;
  2. 然后,将图G进行转置,得到 G T \text G^ \text T GT
  3. 再对 G T \text G^ \text T GT调用DFS算法,但在dfs函数中,对每个顶点的搜索循环里,要以顶点的“结束时间”倒序来搜索;
  4. 最后,深度优先森林中的每一棵树就是一个强连通分支。

算法实例:

  • 第一趟DFS:

在这里插入图片描述

  • 转置后第二趟DFS:

    从原来结束时间最大的A(18)开始,第一个子图探索完成后,开始从剩余顶点中的原来结束时间最大的C(10)开始……接着是F(9)……

    在这里插入图片描述

  • 结果:

    在这里插入图片描述

Kosaraju算法最好理解,但是效率最差,因此另外的常用强连通分支算法有:Tarjan算法、Gabow算法(对Tarjan的改进)。感兴趣可以自行搜索了解。

三、最短路径问题

当我们通过网络浏览网页、 发送电子邮件、 QQ消息传输的时候, 数据会在联网设备(以路由器为例)之间流动,不同的路径花费的代价不同。这些是《计算机网络原理》的内容,在此不做赘述,这里我们只关心其中包含的图算法。如何通过图算法,找到代价最小或者说传播速度最快的路径?

我们可以将网络设备体系表示为一个带权边的图:

  • 路由器作为顶点,路由器之间网络连接作为边;
  • 权重可以包括网络连接的速度、网络负载程度、分时段优先级等影响因素;
  • 作为一个抽象,我们把所有影响因素合成为单一的权重。

在这里插入图片描述

解决数据在路由器网络中选择传播速度最快路径的问题, 就转变为在带权图上最短路径的问题。这个问题与广度优先搜索BFS算法解决的词梯问题相似, 只是在边上增加了权重

1. 最短路径问题:Dijkstra算法

解决带权最短路径问题的经典算法是以发明者命名的“迪杰斯特拉(Dijkstra)算法”。这是一个迭代算法,得出从一个顶点到其余所有顶点的最短路径,很接近于广度优先搜索算法BFS的结果。

具体实现上,在顶点Vertex类中的成员dist用于记录从开始顶点到本顶点的最短
带权路径长度(权重之和),算法对图中的每个顶点迭代一次。

  • 顶点的访问次序由一个优先队列来控制,队列中作为优先级的是顶点的dist属性;

  • 最初,只有开始顶点dist设为0,而其他所有顶点dist设为sys.maxsize(最大整数),全部加入优先队列;

  • 随着队列中每个最低dist顶点率先出队;

  • 并计算它与邻接顶点的权重,会引起其它顶点dist的减小和修改,引起堆重排;

  • 并据更新后的dist优先级再依次出队。

算法实例:

  1. 设u为开始顶点,计算与u相连的其他顶点的权重,并将u出队;
  2. 更新v,x的权重,将较小的x(d=1)出队;
  3. 计算与x相连的v,w,y的权重,v:1+2=3 > 2,因此不更新权重,其余的更新,然后将最小的v出队;
  4. 同时y(d=2)也出队,计算与之相连的w,z,并更新w,z;
  5. 将w,z出队。

在这里插入图片描述

2. python代码实现

form pythonds.graphs import Graph,PriorityQueue,Vertex

def dijkstra(aGraph, start):
    pq = PriorityQueue()
    # 起点的距离设置为0
    start.setDistance(0)
    # 对所有顶点建堆,形成优先队列
    pq.buildHeap([(v.getDistance(), v) for v in aGraph])
    while not pq.isEmpty():
        # 优先队列出队
        currentVert = pq.delMin() 
        # 更新相邻顶点的距离值
        for nextVert in currentVert.getConnections():
            newDist = currentVert.getDistance() + currentVert.getWeight(nextVert)
            if newDist < nextVert.getDistance():
                # 修改出队顶点所邻接顶点的dist,并逐个重排队列
                nextVert.setDistance(newDist)
                nextVert.setPred(currentVert)
                # 重新排列优先队列
                pq.decreaseKey(nextVert, newDist)

需要注意的是,Dijkstra算法只能处理大于0的权重,如果图中出现负数权重,则算法会陷入无限循环。

虽然Dijkstra算法完美解决了带权图的最短路径问题,但实际上Internet的路由器中采用的是其它算法🤨。其中最重要的原因是,Dijkstra算法需要具备整个图的数据,但对于Internet的路由器来说,显然无法将整个Internet所有路由器及其连接信息保存在本地。这不仅是数据量的问题,Internet动态变化的特性也使得保存全图缺乏现实性。

路由器的选径算法(或“路由算法”)对于互联网极其重要,有兴趣可以进一步参考“距离向量路由算法”。

3. Dijkstra算法分析

  • 首先,将所有顶点加入优先队列并建堆,时间复杂度为 O ( ∣ V ∣ ) O(|V|) O(V)
  • 其次,每个顶点仅出队1次,每次delMin花费 O ( log ⁡ ∣ V ∣ ) O(\log |V|) O(logV),一共就是 O ( ∣ V ∣ log ⁡ ∣ V ∣ ) O(|V|\log |V|) O(VlogV)
  • 另外, 每个边关联到的顶点会做一次decreaseKey操作 O ( log ⁡ ∣ V ∣ ) O(\log |V|) O(logV),一共是 O ( ∣ E ∣ log ⁡ ∣ V ∣ ) O(|E|\log |V|) O(ElogV)

上面三个加在一起, 数量级就是 O ( ( ∣ V ∣ + ∣ E ∣ ) log ⁡ ∣ V ∣ ) O((|V|+|E|)\log |V|) O((V+E)logV)

四、最小生成树

本算法涉及到在互联网中网游设计者和网络收音机所面临的问题——信息广播问题。如:网游需要让所有玩家获知其他玩家所在的位置,收音机则需要让所有听众获取直播的音频数据等。

1. 单播解法

信息广播问题最简单的解法是由广播源维护一个收听者的列表(Broadcast list)将每条消息向每个收听者(Listener)发送一次。如下图,每条消息会被发送4次,因为有四个Listener,每个消息都采用最短路径算法到达收听者。

在这里插入图片描述

路由器A会处理4次相同消息,C仅会处理1次;而B/D位于其它3个收听者的最短路
径上,则各会处理转发3次相同消息。这就会产生许多额外流量。

2. 洪水解法

信息广播问题的暴力解法,是将每条消息在路由器间散布出去,所有的路由器都将收到的消息转发到自己相邻的路由器和收听者。显然,如果没有任何限制,这个方法将造成网络洪水灾难,很多路由器和收听者会不断重复收到相同的消息,永不停止!

所以,洪水解法一般会给每条消息附加一个生命值(TTL:Time To Live),初始设置为从消息源到最远的收听者的距离;每个路由器收到一条消息:

  • 如果其TTL值大于0,则将TTL减少1,再转发出去;
  • 如果TTL等于0了,则就直接抛弃这个消息。

TTL的设置防止了灾难发生,但这种洪水解法显然比前述的单播方法所产生的流量还要大。

3. 最小生成树解法

信息广播问题的最优解法,依赖于路由器关系图上选取具有最小权重的生成树(minimum weight spanning tree)

生成树:拥有图中所有的顶点和最少数量的边,并保持连通的子图。

图G(V,E)的最小生成树T定义为:包含所有顶点V,以及E的无圈子集,并且边权重之和最小

下图为上面广播网络图的最小生成树。沿着实线就可以到达每一个顶点,省去了非必须的虚线部分:

在这里插入图片描述

这样信息广播就只需要从A开始,沿着树的路径层次向下传播,就可以达到每个路由器只需要处理1次消息,同时总费用最小。

4. Prim算法

解决如何生成最小生成树问题的Prim算法,属于“贪心算法”,即每步都沿着最小权重的边向前搜索。

构造最小生成树的思路很简单,如果T还不是生成树,则反复做:找到一条最小权重的可以安全添加的边,将边添加到树T。

“可以安全添加”的边,定义为一端顶点在最小生成树中,另一端不在最小生成树中的边,以便保持树的无圈特性

算法实例:

我们来看看Prim是如何求出上面那棵最小生成树的。

  1. 以A为起点,与A相邻的顶点有B、C,其中可以安全添加的边为AB、AC;
  2. 选择权重最小的AB,将AB加入最小生成树;
  3. 与A、B相邻的有C、D、E,BD、和BC权重都是最小,任意选一个,就选BC,加入生成树;
  4. 与B、C相邻的D、E、F,选择BD……
  5. 直到所有顶点都在最小生成树中。

在这里插入图片描述

python代码实现

from pythonds.graphs import PriorityQueue, Graph, Vertex

def prim(G,start):
    # 创建优先队列
    pq = PriorityQueue()
    # 节点最小权重代价默认maxsize
    for v in G:
        v.setDistance(sys.maxsize)
        v.setPred(None)
    # 起点最小权重代价设置为0
    start.setDistance(0)
    pq.buildHeap([(v.getDistance(),v) for v in G])
    while not pq.isEmpty():
        # 取出最小生成树中目前权重最小的节点,作为当前节点
        currentVert = pq.delMin()
        # 遍历节点的所有邻接节点
        for nextVert in currentVert.getConnections():
          newCost = currentVert.getWeight(nextVert)
          # 如果邻接节点是"安全边",并且小于邻接节点原有最小权重代价dist,就更新邻接节点
          if nextVert in pq and newCost<nextVert.getDistance():
              nextVert.setPred(currentVert)
              nextVert.setDistance(newCost)
              pq.decreaseKey(nextVert,newCost)
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花_城

你的鼓励就是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值