数据结构与算法之美笔记(十一)拓扑排序和最短路径

拓扑排序:
解决的问题:有多个事件,事件之间存在依赖关系,我们要确定一个处理事件的顺序,并保证在完成当前事件之前,他所依赖的事件必须已经被执行了、

算法是构建在具体的数据结构之上的。针对这个问题,我们先来看下,如何将问题背景抽象成具体的数据结构?

我们可以把事件之间的依赖关系,抽象成一个有向图。每个事件对应图中的一个顶点,事件之间的依赖关系就是顶点之间的边。

如果a先于b执行,也就是说b依赖于a,那么就在顶点a和顶点b之间,构建一条从a指向b的边。而且,这个图不仅要是有向图,还要是一个有向无环图,也就是不能存在像a->b->c->a这样的循环依赖关系。因为图中一旦出现环,拓扑排序就无法工作了。实际上,拓扑排序本身就是基于有向无环图的一个算法。

拓扑排序的两种实现方法:

  1. Kahn算法
    过程:
    1、我们先遍历一遍各个结点间的情况(在输入数据时就可以进行这个操作了),如果B依赖于A,即在AB间加上一条A指向B的边,并把B的入度加一
    2、找到入度为0的结点,将其放入队列中,这个队列是保存所有入度为0的点的。
    3、开始操作,直到队列为空才停止。我们把队头的能到达的结点的入度都减一,如果那些结点有入度变为0的就加入队列,然后把队头数据加入结果数组,然后移除队头。

  2. 深度优先遍历(其实bfs也可以)
    这个算法包含两个关键部分。

    1. 通过邻接表构造逆邻接表。邻接表中,边s->t表示s先于t执行,也就是t要依赖s。在逆邻接表中,边s->t表示s依赖于t,s后于t执行。为什么这么转化呢?这个跟我们这个算法的实现思想有关。
    2. 这个算法的核心,也就是递归处理每个顶点。对于顶点vertex来说,我们先输出它可达的所有顶点,也就是说,先把它依赖的所有的顶点输出了,然后再输出自己。

复杂度分析:

  1. 每个顶点被访问了一次,每个边也都被访问了一次,所以,Kahn算法的时间复杂度就是O(V+E)(V表示顶点个数,E表示边的个数)。
  2. 每个顶点被访问两次,每条边都被访问一次,所以时间复杂度也是O(V+E)。

注意,这里的图可能不是连通的,有可能是有好几个不连通的子图构成,所以,E并不一定大于V,两者的大小关系不确定。所以,在表示时间复杂度的时候,V、E都要考虑在内。

凡是需要通过局部顺序来推导全局顺序的,一般都能用拓扑排序来解决。除此之外,拓扑排序还能检测图中环的存在。对于Kahn算法来说,如果最后输出出来的顶点个数,少于图中顶点个数图中还有入度不是0的顶点,那就说明,图中存在环。
比如:我们想要知道,数据库中的所有用户之间的推荐关系了,有没有存在环的情况。这个问题,就需要用到拓扑排序算法了。我们把用户之间的推荐关系,从数据库中加载到内存中,然后构建成今天讲的这种有向图数据结构,再利用拓扑排序,就可以快速检测出是否存在环了。


最短路径

单源最短路径算法 Dijkstra
这个我不太好用自己的语言去描述,就直接引用作者的文字了

我们用vertexes数组,记录从起始顶点到每个顶点的距离(dist)。起初,我们把所有顶点的dist都初始化为无穷大(也就是代码中的Integer.MAX_VALUE)。我们把起始顶点的dist值初始化为0,然后将其放到优先级队列中。

我们从优先级队列中取出dist最小的顶点minVertex,然后考察这个顶点可达的所有顶点(代码中的nextVertex)。如果minVertex的dist值加上minVertex与nextVertex之间边的权重w小于nextVertex当前的dist值,也就是说,存在另一条更短的路径,它经过minVertex到达nextVertex。那我们就把nextVertex的dist更新为minVertex的dist值加上w。然后,我们把nextVertex加入到优先级队列中。重复这个过程,直到找到终止顶点t或者队列为空。

predecessor数组的作用是为了还原最短路径,它记录每个顶点的前驱顶点。最后,我们通过递归的方式,将这个路径打印出来。打印路径的print递归代码我就不详细讲了,这个跟我们在图的搜索中讲的打印路径方法一样。如果不理解的话,你可以回过头去看下那一节。

inqueue数组是为了避免将一个顶点多次添加到优先级队列中。我们更新了某个顶点的dist值之后,如果这个顶点已经在优先级队列中了,就不要再将它重复添加进去了。

在这里插入图片描述
时间复杂度: O ( E ∗ l o g V ) O(E*logV) O(ElogV)


做工程不像做理论,一定要给出个最优解。理论上算法再好,如果执行效率太低,也无法应用到实际的工程中。对于软件开发工程师来说,我们经常要根据问题的实际背景,对解决方案权衡取舍。类似出行路线这种工程上的问题,我们没有必要非得求出个绝对最优解。很多时候,为了兼顾执行效率,我们只需要计算出一个可行的次优解就可以了。

问题1:
地图很大,岔路口、道路很多,我们如果对整张图进行求最短路径,会很耗时

方案:虽然地图很大,但是两点之间的最短路径或者说较好的出行路径,并不会很“发散”,只会出现在两点之间和两点附近的区块内。所以我们可以在整个大地图上,划出一个小的区块,这个小区块恰好可以覆盖住两个点,但又不会很大。我们只需要在这个小区块内部运行Dijkstra算法,这样就可以避免遍历整个大图,也就大大提高了执行效率。

问题2:
如果两点距离比较远,从北京海淀区某个地点,到上海黄浦区某个地点,那上面的这种处理方法,显然就不工作了,毕竟覆盖北京和上海的区块并不小。

方案:对于这样两点之间距离较远的路线规划,我们可以把北京海淀区或者北京看作一个顶点,把上海黄浦区或者上海看作一个顶点,先规划大的出行路线。比如,如何从北京到上海,必须要经过某几个顶点,或者某几条干道,然后再细化每个阶段的小路线。


最少时间
其实就是在最短路径的基础上,把边权值改成走完这条路需要花费的时间


最少红绿灯
把边权改为1


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值