最近本人终于做回了自己的老本行写代码,先从复习数据结构与算法刷题做起吧。工程/语言什么的也在学了,一并记录。由于老年腱鞘炎严重,不得不放弃传统手写而采用打字的方法。
题目是这样的:(来自Leetcode 210 课程表II)
现在你总共有
numCourses
门课需要选,记为0
到numCourses - 1
。给你一个数组prerequisites
,其中prerequisites[i] = [ai, bi]
,表示在选修课程ai
前 必须 先选修bi
。
- 例如,想要学习课程
0
,你需要先完成课程1
,我们用一个匹配来表示:[0,1]
。返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
很容易就能想到,将所有的课程都看成一个节点,选修的先后关系看成一条有向边,我们可以得到一个有向图。在什么情况下不可能完成所有课程呢?最简单的情况是,课程1需要先修课程2,同时课程2需要先修课程1,形成闭环。那么,需要求得的结果便是:输出一组节点的顺序,使得在该排序中各边的先后顺序不能满足,同时当存在环时返回错误结果。
这是一个标准的拓扑排序问题。
百度百科里拓扑排序是这么描述它的:
对一个有向无环图 ( Directed Acyclic Graph 简称 DAG ) G 进行拓扑排序,是将 G
中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v ,若边 < u , v > ∈ E ( G ),则 u 在线性序列中出现在 v
之前。通常,这样的线性序列称为满足拓扑次序 ( Topological Order )
的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
那么,如何求解呢?
有以下两种方法,分别需要用到queue以及stack两种数据结构,对应BFS算法以及DFS算法。
1、拓扑排序的BFS方法
首先,统计所有点的入度,选择其中入度为0的点,并将其加入一个队列之中。然后重复以下操作:
弹出队首元素,访问其所有邻节点并将其入度-1(等同于将节点从图中删除),此时若其邻节点在删除后有入度为0的,将其加入队列,直到队列为空。
若弹出的总元素数量少于总点数并且队列已经为空,说明图中存在环,不存在拓扑排序。
否则,则弹出的顺序即为该有向无环图的一个拓扑排序。
2、拓扑排序的DFS方法
首先,我们将节点的状态分为三种:
(1)待探索,说明此时DFS还没有扩展到这个节点。
(2)探索中,说明此时DFS已经扩展到了这个节点,但还没有搜索完成其所有分支。
(3)已探索,说明这个节点已经探索完了。
同样的,我们从任意一个入度为0的节点开始探索,如果不存在这样的节点那么也就不存在拓扑排序了。将其作为当前节点。同时,我们构建一个结果栈Stack。
若当前节点存在邻居,可能有三种情况:
(1)存在邻居未探索,此时深入并将该邻居标记为探索中,并作为当前节点。
(2)所有的邻居都已探索,此时将当前节点标记为已探索,将其压入Stack并回溯。
(3)存在一个邻居的状态为探索中,此时说明存在环,没有拓扑排序。
重复以上操作直至所有节点都压入Stack中。
此时将该结果栈的元素逐一弹出,即可得到该有向无环图的一种拓扑排序。
附:stl中stack、queue的用法
(1)stack
用法为stack<T> st ,主要接口有push(x)压入元素,pop()弹出元素(返回值为void),top()返回栈顶元素以及empty()和size()。位于头文件<stack>中
(2)queue
用法为queue<T> que ,主要接口有push(x)压入元素,pop()弹出队首元素,front()、end()分别取队首队尾元素以及empty()和size(),位于头文件<queue>中