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

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

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

环是和方向有关的,从一个点出发能回到自己,这是环。那么所有活动可以排成一个可行线性序列,这个序列就是拓扑序列。注意,有时候拓扑序并不是唯一的,比如在这个例子中,先学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 应用元年来了,开发者及企业的路径及发展方向!

,
    推荐阅读
  • 山药怎么做脆(脆山药做法)

    我们一起去了解并探讨一下这个问题吧!山药去皮切片,胡萝卜也皮切片,一同放入热水里焯2分钟,然后放凉水冲洗干净。锅内放入适量的植物油,烧热后放入事先焯好的山药和胡萝卜,翻炒片刻。调入适量的盐,翻炒均匀,再加入适量的生粉水勾个薄欠炒均就可以装盘了。

  • 全球美术学校排名(合肥世界外国语学校)

    冬日清寒,风景这边独好!高中美术班自创办以来,从蹒跚起步到如今的花开校园,师资力量和生源质量的不断提升,发展的每步历程,无不透露出世外办学思路的睿智,教师团队的精诚。这棵嫩苗已茁壮成长起来,坚守着这个美丽的家园,不断创造出一个又一个辉煌!色彩靓丽中不失动感,素描骨感里透着清新,速写线条间流动着喜欢,设计理念时勾勒着梦幻!2019届高三美术班,精致而又温婉,才气内敛,执笔如仙,今日与君共赏!

  • 韭菜鸡蛋捞面条怎么做(韭菜鸡蛋捞面条的做法)

    跟着小编一起来看一看吧!韭菜鸡蛋捞面条怎么做主料:面条(生)200克。辅料:韭菜1把、鸡蛋2个、香油10克、盐2克、鸡精1克、醋适量。切好的韭菜和葱放入碗里,放入盐、鸡精备用。鸡蛋打入碗内搅散,锅内烧开水,倒入鸡蛋液。蛋液会立刻浮起来,看着全部凝固了熟了,用捞出来放到韭菜碗里。倒入适量的开水,盖上盖子焖一下。下入面条煮熟,捞出过冷水。搅匀,吃的时候把汤浇在面条上即可。

  • 脸色暗黄有斑怎么办月经不调(掉头发脸色差长斑是气血不足的表现)

    气的作用主要是温养机体和抵御外邪的入侵,同时参与脏腑功能的新陈代谢。气血不足的结果会导致脏腑功能的减退,引起早衰的病变。一旦气血不足时,首先人体会自动保证五脏的气血供应,从而减少对四肢末节的气血供应。黄芪有益气、固本,敛汗、生肌、利水、消水肿等功效。红枣可补中益气、养血安神,可用于脾胃气虚、血虚失眠多梦等症。做法:黄芪和当归以4:1的比例用量,红枣适量。黄芪当归红枣茶主要功效为补养气血。

  • 潮汕人在泰国排行(你知道自己与泰国人的)

    据一位西方学者估计,直至1830年,曼谷城50万居民中有40万为华侨,其中主要为潮汕人,甚至出现了“潮汕话是泰国第二语言”的说法。其中最有潮汕特色的当属大峰祖师。传说航海的人见林默身着红装飞翔在海上,救助遇难呼救的人,所以人们修建了供奉林默的庙宇,称之为妈祖庙。在出海前,船主会到妈祖庙中请香,以保佑自己航行顺利。妈祖并非起源于潮汕,但在宋元时期已经在潮汕流传开来,当地人民亲切叫其为七圣妈。

  • 梦幻西游三维版普陀山带什么宝宝(赶紧来看看)

    梦幻西游三维版普陀山带什么宝宝?梦幻西游三维版普陀山带这三个宝宝:小悟空—吸血鬼—孔雀妖/狂豹,现在小编就来说说关于梦幻西游三维版普陀山带什么宝宝?基本上前期都是用小悟空去升级的,是一个万金油类的宠物,谁让它好用呢,输出能力强啊,而且就算受伤了,普陀还给它加血啥的,根本不愁啊,所以还是非常厉害的。

  • 酸辣魔芋豆腐(需要准备哪些食材)

    酸辣魔芋豆腐食材:魔芋:400克;酸菜:200克;小米椒:1个;生姜:1块;芹菜:2根;香葱:1根;花椒:10粒;盐:1茶勺;鸡精:1茶勺;芝麻辣椒油:两勺;白糖:半勺;郫县豆瓣酱:1大勺;食用油:适量。酸菜洗净切细,芹菜、葱白、生姜切丝,葱切花,小米椒切圈。加入郫县豆瓣酱,提入半勺白糖炒香。下入酸菜及芹菜丝煮开,小火再焖煮10分钟。装盘后淋入芝麻辣椒油即可上桌开吃。

  • 洛氏硬度如何换算(什么是洛氏硬度)

    什么是洛氏硬度洛氏硬度RockwellHardness用锥顶角为120°的金刚石圆锥或Ø1.588mm和Ø3.176mm淬火钢球作压头和载荷配合使用,在10kgf初载荷和60、100或150kgf力总载荷(即初载荷。

  • 我们是亲兄弟原唱歌谱(我们是亲兄弟)

    接下来我们就一起去研究一下吧!我们是亲兄弟原唱歌谱游戏名:pvz95版注:95版土豆类爆炸范围是3*3格和樱桃炸弹一样!

  • 防暑冷饮三豆饮(食疗方三豆饮是怎么做的)

    我们一起去了解并探讨一下这个问题吧!防暑冷饮三豆饮材料:黑豆150克、绿豆150克、赤小豆150克、另外再加60克甘草,白糖适量。另一种是将三种豆洗净浸泡至涨后混合磨成浆,加水适量煮沸,以白糖调味饮服,每日2次,早晚服用,可长期食用。