之前刷了不少leetcode的题目,现在开始春招了,特地把之前写的代码上传了github:https://github.com/czy36mengfei/leetcode,欢迎大家下载、star。
知识拓展: 拓扑排序
拓扑排序并非一种排序算法,它能得到一个 AOV 网络的拓扑序列,用于判断有向无环图中是否有环,即可以判断一系列活动是否有循环依赖;
具体例子:去店里吃饭的问题:顾客要求先吃饭再付钱,商家要求先收钱再做菜,这就是循环依赖,拓扑排序就可以帮助我们判断是否形成环。
步骤:找无前驱的结点(即入度为 0 的结点),一个一个地删去(使用队列或者栈),删的时候,把邻居结点的入度 -1。
“拓扑排序”用于对有先后顺序的任务的输出,如果先后顺序形成一个环,那么就表示这些任务头尾依赖,就永远不能完成。
现在你总共有 n 门课需要选,记为
0
到n-1
。在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:
[0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]] 输出: true 解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。 123
示例 2:
输入: 2, [[1,0],[0,1]] 输出: false 解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。 123
说明:
- 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
- 你可以假定输入的先决条件中没有重复的边。
提示:
- 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
- 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
- 拓扑排序也可以通过 BFS 完成。
方法一:拓扑排序(Kahn 算法)
拓扑排序实际上应用的是贪心算法,贪心算法简而言之:每一步最优,全局就最优)。具体到拓扑排序,每一次都输出入度为 0 的结点,并移除它、修改它指向的结点的入度,依次得到的结点序列就是拓扑排序的结点序列。如果图中还有结点没有被移除,则说明“不能完成所有课程的学习”。
拓扑排序保证了每个活动(在这题中是“课程”)的所有前驱活动都排在该活动的前面,并且可以完成所有活动。拓扑排序的结果不唯一。拓扑排序还可以用于检测一个有向图是否有环。相关的概念还有 AOV 网,这里就不展开了。
具体做如下:
1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 00 的结点放入队列。
2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,并且将这个结点的所有邻接结点(它指向的结点)的入度减 1,在减 1 以后,如果这个被减 1 的结点的入度为 0 ,就继续入队。
3、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等即可。
class Solution(object): # 思想:该方法的每一步总是输出当前无前趋(即入度为零)的顶点 def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: """ :type numCourses: int 课程门数 :type prerequisites: List[List[int]] 课程与课程之间的关系 :rtype: bool """ # 课程的长度 clen = len(prerequisites) if clen == 0: # 没有课程,当然可以完成课程的学习 return True # 步骤1:统计每个顶点的入度 # 入度数组,记录了指向它的结点的个数,一开始全部为 0 in_degrees = [0 for _ in range(numCourses)] # 邻接表,使用散列表是为了去重 adj = [set() for _ in range(numCourses)] # 想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1] # [0, 1] 表示 1 在先,0 在后 # 注意:邻接表存放的是后继 successor 结点的集合 for second, first in prerequisites: in_degrees[second] += 1 adj[first].add(second) # 步骤2:拓扑排序开始之前,先把所有入度为 0 的结点加入到一个队列中 # 首先遍历一遍,把所有入度为 0 的结点都加入队列 queue = [] for i in range(numCourses): if in_degrees[i] == 0: queue.append(i) counter = 0 while queue: top = queue.pop(0) counter += 1 # 步骤3:把这个结点的所有后继结点的入度减去 1,如果发现入度为 0 ,就马上添加到队列中 for successor in adj[top]: in_degrees[successor] -= 1 if in_degrees[successor] == 0: queue.append(successor) return counter == numCourses
方法二:深度优先遍历
这里要使用逆邻接表。其实就是检测这个有向图中有没有环,只要存在环,这些课程就不能按要求学完。
具体方法是:
第 1 步:构建逆邻接表;
第 2 步:递归处理每一个还没有被访问的结点,具体做法很简单:对于一个结点来说,先输出指向它的所有顶点,再输出自己。
第 3 步:如果这个顶点还没有被遍历过,就递归遍历它,把所有指向它的结点都输出了,再输出自己。注意:当访问一个结点的时候,应当先递归访问它的前驱结点,直至前驱结点没有前驱结点为止。
class Solution(object): # 这里使用逆邻接表 def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: """ :type numCourses: int 课程门数 :type prerequisites: List[List[int]] 课程与课程之间的关系 :rtype: bool """ # 课程的长度 clen = len(prerequisites) if clen == 0: # 没有课程,当然可以完成课程的学习 return True # 深度优先遍历,判断结点是否访问过 # 这里要设置 3 个状态 # 0 就对应 False ,表示结点没有访问过 # 1 就对应 True ,表示结点已经访问过,在深度优先遍历结束以后才置为 1 # 2 表示当前正在遍历的结点,如果在深度优先遍历的过程中, # 有遇到状态为 2 的结点,就表示这个图中存在环 visited = [0 for _ in range(numCourses)] # 逆邻接表,存的是每个结点的前驱结点的集合 # 想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1] # 1 在前,0 在后 inverse_adj = [set() for _ in range(numCourses)] for second, first in prerequisites: inverse_adj[second].add(first) for i in range(numCourses): # 在遍历的过程中,如果发现有环,就退出 if self.__dfs(i, inverse_adj, visited): return False return True def __dfs(self, vertex, inverse_adj, visited): """ 注意:这个递归方法的返回值是返回是否有环 :param vertex: 结点的索引 :param inverse_adj: 逆邻接表,记录的是当前结点的前驱结点的集合 :param visited: 记录了结点是否被访问过,2 表示当前正在 DFS 这个结点 :return: 是否有环,返回 True 表示这个有向图有环 """ # 2 表示这个结点正在访问 if visited[vertex] == 2: # 表示遇到环 return True if visited[vertex] == 1: return False visited[vertex] = 2 for precursor in inverse_adj[vertex]: # 如果有环,就返回 True 表示有环 if self.__dfs(precursor, inverse_adj, visited): return True # 1 表示访问结束 # 先把 vertex 这个结点的所有前驱结点都输出之后,再输出自己 visited[vertex] = 1 return False
点我有惊喜
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
说明:
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
提示:
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
拓扑排序也可以通过 BFS 完成。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule-ii