农业资讯网
当前位置: 首页 农业百科

拓扑排序求总路径(揭开拓扑排序的神秘面纱)

时间:2023-05-22 作者: 小编 阅读量: 1 栏目名: 农业百科

环是和方向有关的,从一个点出发能回到自己,这是环。那么所有活动可以排成一个可行线性序列,这个序列就是拓扑序列。注意,有时候拓扑序并不是唯一的,比如在这个例子中,先学C1再学C2,和先C2后C1都行,都是这个图的正确的拓扑序,但这是两个顺序了。一个常规顺序的简简单单的queue就够用了。也就意味着C8此时没有了任何依赖,可以放到我们的queue里等待执行了。把它们放入queue,继续执行到直到queue为空即可。

作者 | 小齐本齐

责编 | Carol

来源 | 码农田小齐

Topological sort 又称 Topological order,这个名字有点迷惑性,因为拓扑排序并不是一个纯粹的排序算法,它只是针对某一类图,找到一个可以执行的线性顺序。

这个算法听起来高大上,如今的面试也很爱考,比如当时我在面我司时有整整一轮是基于拓扑排序的设计。

但它其实是一个很好理解的算法,跟着我的思路,让你再也不会忘记她。

有向无环图

刚刚我们提到,拓扑排序只是针对特定的一类图,那么是针对哪类图的呢?

答:Directed acyclic graph (DAG),有向无环图。即:

  1. 这个图的边必须是有方向的;

  2. 图内无环。

那么什么是方向呢?

比如微信好友就是有向的,你加了他好友他可能把你删了你却不知道。。。那这个朋友关系就是单向的。。

什么是环?环是和方向有关的,从一个点出发能回到自己,这是环。

所以下图左边不是环,右边是。

那么如果一个图里有环,比如右图,想执行1就要先执行3,想执行3就要先执行2,想执行2就要先执行1,这成了个死循环,无法找到正确的打开方式,所以找不到它的一个拓扑序。

总结:

  • 如果这个图不是 DAG,那么它是没有拓扑序的;

  • 如果是 DAG,那么它至少有一个拓扑序;

  • 反之,如果它存在一个拓扑序,那么这个图必定是 DGA.

所以这是一个充分必要条件。

拓扑排序

那么这么一个图的「拓扑序」是什么意思呢?

我们借用百度百科[1]的这个课程表来说明。

这里有 9 门课程,有些课程是有先修课程的要求的,就是你要先学了「最右侧这一栏要求的这个课」才能再去选「高阶」的课程。

那么这个例子中拓扑排序的意思就是:

就是求解一种可行的顺序,能够让我把所有课都学了。

那怎么做呢?

首先我们可以用来描述它,图的两个要素是顶点和边,那么在这里:

顶点:每门课

边:起点的课程是终点的课程的先修课

画出来长这个样:

这种图叫 AOV(Activity On Vertex) 网络,在这种图里:

顶点:表示活动;

边:表示活动间的先后关系

所以一个 AOV 网应该是一个 DAG,即有向无环图,否则某些活动会无法进行。

那么所有活动可以排成一个可行线性序列,这个序列就是拓扑序列。

那么这个序列的实际意义是:

按照这个顺序,在每个项目开始时,能够保证它的前驱活动都已完成,从而使整个工程顺利进行。

回到我们这个例子中:

  1. 我们一眼可以看出来要先学 C1, C2,因为这两门课没有任何要求嘛,大一的时候就学呗;

  2. 大二就可以学第二行的 C3, C5, C8 了,因为这三门课的先修课程就是 C1, C2,我们都学完了;

  3. 大三可以学第三行的 C4, C9;

  4. 最后一年选剩下的 C6, C7。

这样,我们就把所有课程学完了,也就得到了这个图的一个拓扑排序

注意,有时候拓扑序并不是唯一的,比如在这个例子中,先学 C1 再学 C2,和先 C2 后 C1 都行,都是这个图的正确的拓扑序,但这是两个顺序了。

所以面试的时候要问下面试官,是要求解任意解,还是列出所有解。

我们总结一下,

在这个图里的表示的是一种依赖关系,如果要修下一门课,就要先把前一门课修了。

这和打游戏里一样一样的嘛,要拿到一个道具,就要先做 A 任务,再完成 B 任务,最终终于能到达目的地了。

算法详解

在上面的图里,大家很容易就看出来了它的拓扑序,但当工程越来越庞大时,依赖关系也会变得错综复杂,那就需要用一种系统性的方式方法来求解了。

那么我们回想一下刚刚自己找拓扑序的过程,为什么我们先看上了 C1, C2?

因为它们没有依赖别人啊,也就是它的入度为 0.

入度:顶点的入度是指「指向该顶点的边」的数量;

出度:顶点的出度是指该顶点指向其他点的边的数量。

所以我们先执行入度为 0 的那些点,

那也就是要记录每个顶点的入度。

因为只有当它的入度为 0的时候,我们才能执行它。

在刚才的例子里,最开始 C1, C2 的入度就是 0,所以我们可以先执行这两个。

那在这个算法里第一步就是得到每个顶点的入度。

Step0: 预处理得到每个点的入度

我们可以用一个 HashMap 来存放这个信息,或者用一个数组会更精巧。

在文中为了方便展示,我就用表格了:

Step1

拿到了这个之后,就可以执行入度为 0 的这些点了,也就是 C1, C2.

那我们把可以被执行的这些点,放入一个待执行的容器里,这样之后我们一个个的从这个容器里取顶点就好了。

至于这个容器究竟选哪种数据结构,这取决于我们需要做哪些操作,再看哪种数据结构可以为之服务。

那么首先可以把[C1, C2]放入容器中,

然后想想我们需要哪些操作吧!

我们最常做的操作无非就是把点放进来,把点拿出去执行了,也就是需要一个offer 和poll操作比较高效的数据结构,那么 queue 就够用了。

(其他的也行,放进来这个容器里的顶点的地位都是一样的,都是可以执行的,和进来的顺序无关,但何必非得给自己找麻烦呢?一个常规顺序的简简单单的 queue 就够用了。)

然后就需要把某些点拿出去执行了。

【划重点】当我们把 C1 拿出来执行,那这意味这什么?

答:意味着「以 C1 为顶点」的「指向其他点」的「边」都消失了,也就是 C1 的出度变成了 0.

如下图,也就是这两条边可以消失了。

那么此时我们就可以更新 C1 所指向的那些点也就是 C3 和 C8 的 入度 了,更新后的数组如下:

那我们这里看到很关键的一步,C8 的入度变成了 0!

也就意味着 C8 此时没有了任何依赖,可以放到我们的 queue 里等待执行了。

此时我们的 queue 里就是:[C2, C8].

Step2

下一个我们再执行 C2,

那么 C2 所指向的 C3, C5 的 入度-1,

更新表格:

也就是 C3 和 C5 都没有了任何束缚,可以放进 queue 里执行了。

queue 此时变成:[C8, C3, C5]

Step3

那么下一步我们执行 C8,

相应的 C8 所指的 C9 的入度-1.更新表格:

那么 C9 没有了任何要求,可以放进 queue 里执行了。

queue 此时变成:[C3, C5, C9]

Step4

接下来执行 C3,

相应的 C3 所指的 C4 的入度-1.更新表格:

但是 C4 的入度并没有变成 0,所以这一步没有任何点可以加入 queue。

queue 此时变成 [C5, C9]

Step5

再执行 C5,

那么 C5 所指的 C4 和 C6 的入度- 1.更新表格:

这里 C4 的依赖全都消失啦,那么可以把 C4 放进 queue 里了:

queue = [C9, C4]

Step6

然后执行 C9,

那么 C9 所指的 C7 的入度- 1.

这里 C7 的入度并不为 0,还不能加入 queue,

此时 queue = [C4]

Step7

接着执行 C4,

所以 C4 所指向的 C6 和 C7 的入度-1,更新表格:

C6 和 C7 的入度都变成 0 啦!!把它们放入 queue,继续执行到直到 queue 为空即可。

总结

好了,那我们梳理一下这个算法:

数据结构这里我们的入度表格可以用 map 来存放

Map: <key = Vertex, value = 入度>

但实际代码中,我们用一个 int array 来存储也就够了,数组的 index 表示每个顶点,数组里的数值来表示每个顶点的入度,这样比 Map 更精巧。

然后用了一个普通的 queue,用来存放可以被执行的那些 node.

过程

我们把入度为 0 的那些顶点放入 queue 中,然后通过每次执行 queue 中的顶点,就可以让依赖这个被执行的顶点的那些点的 入度-1,如果有顶点的入度变成了 0,就可以放入 queue 了,直到 queue 为空。

细节

这里有几点实现上的细节:

当我们 check 是否有新的顶点的 入度 == 0 时,没必要过一遍整个 map 或者数组,只需要 check 刚刚改动过的就好了。

另一个是如果题目没有给这个图是 DAG 的条件的话,那么有可能是不存在可行解的,那怎么判断呢?很简单的一个方法就是比较一下最后结果中的顶点的个数和图中所有顶点的个数是否相等,或者加个计数器,如果不相等,说明就不存在有效解。所以这个算法也可以用来判断一个图是不是有向无环图。

很多题目给的条件可能是给这个图的 edge list,也是表示图的一种常用的方式。那么给的这个 list 就是表示图中的边。这里要注意审题哦,看清楚是谁 depends on 谁。其实图的题一般都不会直接给你这个图,而是给一个场景,需要你把它变回一个图。

时间复杂度

注意:对于图的时间复杂度分析一定是两个参数,因为图的顶点数和边的数量没有固定的关系,然而面试的时候很多同学张口就是 O(n)...

对于有 v 个顶点和 e 条边的图来说,

第一步,预处理得到 map 或者 array,需要过一遍所有的边才行,所以是 O(e);

第二步,把 入度 == 0 的点入队出队的操作是 O(v),如果是一个 DAG,那所有的点都需要入队出队一次;

第三步,每次执行一个顶点的时候,要把它指向的那条边消除了,这个总共执行 e 次;

总:O(ve)

空间复杂度

用了一个数组来存所有点的 indegree,之后的 queue 也是最多把所有的点放进去,所以是 O(v).

代码

关于这课程排序的问题,Leetcode 上有两道题,一道是 207,问你能否完成所有课程,也就是问拓扑排序是否存在;另一道是 210 题,是让你返回任意一个拓扑顺序,如果不能完成,那就返回一个空 array。

这里我们以 210 这道题来写,更完整也更常考一些。

这里给的 input 就是我们刚刚说到的 edge list.

Example 1.

Input: 2, [[1,0]]

Output: [0,1]

Explanation: 这里一共2门课,1的先修课程是0. 所以正确的选课顺序是[0, 1].

Example 2.

Input: 4, [[1,0],[2,0],[3,1],[3,2]]

Output: [0,1,2,3] or [0,2,1,3]

Explanation: 这里这个例子画出来如下图

Example 3.

Input: 2, [[1,0],[0,1]]

Output:

Explanation: 这课没法上了

class Solution {

public int findOrder(int numCourses, int[][] prerequisites) {

int res = new int[numCourses];

int indegree = new int[numCourses];

// get the indegree for each course

for(int[] pre : prerequisites) {

indegree[pre[0]];

}

// put courses with indegree == 0 to queue

Queue<Integer> queue = new ArrayDeque<>;

for(int i = 0; i < numCourses; i) {

if(indegree[i] == 0) {

queue.offer(i);

}

}

// execute the course

int i = 0;

while(!queue.isEmpty) {

Integer curr = queue.poll;

res[i] = curr;

// remove the pre = curr

for(int[] pre : prerequisites) {

if(pre[1] == curr) {

indegree[pre[0]] --;

if(indegree[pre[0]] == 0) {

queue.offer(pre[0]);

}

}

}

}

return i == numCourses ? res : new int{};

}

}

还是附上题目吧,just in case, if you want to see the details.

另外,拓扑排序还可以用 DFS - 深度优先搜索 来实现,限于篇幅就不在这里展开了,大家可以参考GeeksforGeeks[2]的这个资料。

实际应用

我们上文已经提到了它的一个 use case,就是选课系统,这也是最常考的题目。

而拓扑排序最重要的应用就是关键路径问题,这个问题对应的是 AOE (Activity on Edge) 网络。

AOE 网络:顶点表示事件,边表示活动,边上的权重来表示活动所需要的时间。

AOV 网络:顶点表示活动,边表示活动之间的依赖关系。

在 AOE 网中,从起点到终点具有最大长度的路径称为关键路径,在关键路径上的活动称为关键活动。AOE 网络一般用来分析一个大项目的工序,分析至少需要花多少时间完成,以及每个活动能有多少机动时间。

具体是怎么应用分析的,大家可以参考这个视频[3] 的14分46秒,这个例子还是讲的很好的。

其实对于任何一个任务之间有依赖关系的图,都是适用的。

比如 pom 依赖引入 jar 包时,大家有没有想过它是怎么导进来一些你并没有直接引入的 jar 包的?比如你并没有引入 aop 的 jar 包,但它自动出现了,这就是因为你导入的一些包是依赖于 aop 这个 jar 包的,那么 maven 就自动帮你导入了。

其他的实际应用,知乎上专门有个帖子[4],在这里我总结一下:

  1. 语音识别系统的预处理;

  2. 管理目标文件之间的依赖关系,就像我刚刚说的 jar 包导入;

  3. 深度学习中的网络结构处理。

如有其他补充,欢迎大家在评论区不吝赐教。

以上就是本文的全部内容了,拓扑排序是非常重要也是非常爱考的一类算法,面试大厂前一定要熟练掌握。

今日福利

遇见大咖

由 CSDN 全新专为技术人打造的高端对话栏目《大咖来了》来啦!

CSDN 创始人&董事长、极客帮创投创始合伙人蒋涛携手京东集团技术副总裁、IEEE Fellow、京东人工智能研究院常务副院长、深度学习及语音和语言实验室负责人何晓冬,来也科技 CTO 胡一川,共话中国 AI 应用元年来了,开发者及企业的路径及发展方向!

,
    推荐阅读
  • 抹茶红豆酥饼制作方法(抹茶红豆酥饼制作方法是什么)

    接下来我们就一起去研究一下吧!抹茶红豆酥饼制作方法把水油酥的材料放入面包机桶内揉面15分钟左右。油酥的材料混合均匀后揉成团。把水油酥和油酥面团盖上保鲜膜醒30分钟。油酥和水油酥各分成30个面团。擀成长舌状卷起,全部都做好。红豆馅分成25克一个,取一个酥饼皮用擀面杖擀开。放入豆沙馅,像包包子一样包好。收口朝下放置,稍稍压扁。烤箱160度预热好后,放入烤箱中层160度烤25分钟即可。

  • 一般油表亮了之后还可以跑多久(三秒知识点油表灯亮了)

    当油表灯亮起的时候,大多数新手司机都会怀着紧张的心情找加油站。但对于有经验的老司机来说,油表灯亮了其实不算什么事。有些更人性化的,还会标明大约还能行驶多少公里。以普通油耗标准来算,当油表灯亮后,汽车基本上还能再跑50~90公里左右。通常油表亮灯后,行驶20公里之内还是很安全的,超出这个范围就要小心行驶,最好提前准备好汽油。但是还是提醒各位小伙伴随时注意自己的油箱指示有备无患哦咱们下期见啦!

  • 梭子蟹死了怎么保存(梭子蟹死了的保存方法)

    以下内容希望对你有帮助!梭子蟹死了怎么保存刚死的梭子蟹最好的保存办法是洗净后蒸熟再冷冻。死的梭子蟹容易脱水,不蒸就直接冻的话,容易造成蟹肉失水,等到再解冻后,蟹壳会变得空空的,黄和膏也会变成液态流出,整个变成空壳,先蒸再冻的话可以有效的锁住水分。不过刚死的梭子蟹建议尽快食用。

  • 火龙果幼苗怎么施肥(火龙果幼苗如何施肥)

    火龙果幼苗怎么施肥给火龙果幼苗施肥的话,一定要选择好时机,原创通常是在扦插买苗出现发芽时才能施肥,这样可以让其快速爆出新芽来。可以为幼苗追施一点尿素肥,它能很好地促进火龙果小苗的生长,尤其对生根发芽非常有帮助。而且尿素肥的使用方法也非常简单,只不过火龙果小苗还很稚嫩、脆弱,一定要控制好浓度,确保肥液足够稀薄。一般取2克尿素肥,按照1000倍兑水稀释,充分溶解摇匀后浇在扦插基质里即可。

  • 电脑屏幕保护怎么取消(电脑屏幕保护取消方法)

    首先,在电脑中找到“计算机”;,现在小编就来说说关于电脑屏幕保护怎么取消?下面内容希望能帮助到你,我们来一起看看吧!在右边找到“更改屏幕保护程序”,选择打开;打开“更改屏幕保护程序”之后,找到“屏幕保护程序”,点击“屏幕保护程序”下面的下拉框;点击“屏幕保护程序”下面的下拉框之后,选择“无”;选择“无”之后,单击“确定”,保存修改;单击“确定”之后,我们已成功取消电脑屏幕保护。

  • 2000元左右手机(2022最值得买的手机推荐)

    搭载骁龙870这种次旗舰级别芯片的手机,价格跳水至1999元,一下子聚焦大量的关注。购买建议:在轻薄特性、外观颜值、屏幕和摄像方面,realmeX7Pro至尊版非常有竞争力。现在价格有所回落,更突显了它的性价比。优势:iQOONeo5SE搭载口碑不俗的高通骁龙870处理器,性能输出绝对很足的,玩起游戏来,不拖泥带水。购买建议:RedmiK40是2021年将旗舰性能拉开2000元价位“先锋”,不过压低价格,必定要阉割掉一些配置的。

  • 杨梅树什么时候开花结果(杨梅树开花结果的时间)

    下面内容希望能帮助到你,我们来一起看看吧!杨梅树什么时候开花结果杨梅4月开花,6-7月果实成熟。杨梅是杨梅科,杨梅属植物,常绿乔木,树皮黄灰黑色,叶倒披针形,先端钝。杨梅为雌雄异株,就是说雌树开雌性花,结果实;雄性树开雄性花,不结果。而且杨梅的花为葇荑花序。

  • 人之初性本善做人道理很现实(人心难猜又难看)

    上山猛虎易见真心,下水毒蛇不认六亲。社会没有遮天物,只有一物降一物。白酒红人面,黄金黑人心。心慈手软,只会让自己摔得更惨。一富遮千丑,一穷毁所有。日久不一定生情,但一定会见人心。世界上的所有事情都是暂时的,如果事事顺心,那就好好享受;如果发生意外,不要过于担心,一切都会过去的。接受自己的不完美,也接受别人的不完美。(图片来自于网络,若有侵犯了您的合法权益,请联系我们,我们将及时更正、删除。

  • 小暑种什么农作物(哪些农作物小暑种植呢)

    下面更多详细答案一起来看看吧!小暑种什么农作物小暑适合种的农作物有很多种,例如黄瓜,大白菜,生菜,菜花,胡萝卜等等。种下后要加强管理,及时浇水补水,满足对水分的需求。若是遇到连续的下雨天,要及时排出地块中的积水,避免抑制生长。此外,还要及时补充养分,养分足,环境适宜这样才可更好的生长,后期产量更高。

  • 韩信出装铭文以及思路和打法(第一代韩信的铭文出装现在还能用吗)

    接下来小编就给大家介绍一下韩信最古老的出装和铭文。蓝色10狩猎、绿色10鹰眼、红色10异变铭文上是用的百穿攻速铭文,异变、鹰眼和狩猎这也是韩信最初使用的铭文,高额的穿透能够让韩信在前期就拥有比较可观的输出,攻速和移速的加成也对韩信的刷野和Gank有很大的帮助。泣血之刃能提供续航能力,破军也能够提供一个高额的输出。