leetcode中经常出现的题型之一。其中,拓扑排序的概念可以参考这里,这里主要总结一下前300题中出现的几个关于拓扑排序的题,以待之后复习的时候查找。
leetcode207 课程表
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?示例 1:
输入: 2, [[1,0]] 输出: true 解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]] 输出: false 解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
说明:
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。 你可以假定输入的先决条件中没有重复的边。
提示:
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。 拓扑排序也可以通过 BFS 完成。
这里涵盖了两种常见的解决拓扑排序的方法,分别是记忆化递归和入度表,由于这里讲的已经非常好就不再多写,直接给出解决的代码。
记忆化递归法:
from collections import deque
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
adjacent = [[] for i in range(numCourses)]
visited = [0] * numCourses
for cur,pre in prerequisites:
adjacent[pre].append(cur)
def dfs(node):
if visited[node] == 1: return False
if visited[node] == -1: return True
visited[node] = 1
for neighbor in adjacent[node]:
if not dfs(neighbor): return False
visited[node] = -1
return True
for i in range(numCourses):
if not dfs(i): return False
return True
入度表法:
from collections import deque
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
from collections import deque
indegree = [0 for _ in range(numCourses)]
adjacent = [[] for _ in range(numCourses)]
##建立入度表
for cur,pre in prerequisites:
indegree[cur] += 1
adjacent[pre].append(cur)
##建立入度为0的序列
queue = deque()
for element in range(len(indegree)):
if not indegree[element]:
queue.append(element)
##遍历序列
while queue:
element = queue.popleft()
numCourses -= 1
for neighbors in adjacent[element]:
indegree[neighbors] -= 1
if indegree[neighbors] == 0: queue.append(neighbors)
return numCourses == 0
leetcode208 课程表2
现在你总共有 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 完成。
如果我们要返回的不仅仅是排序的可能性,而是排序结果,应该怎么办?其实很容易想通,在入度表元素变为零/某个元素的记忆化递归完成的时候,就说明这个元素已经“无牵无挂”,没有前向节点或前向节点已经加入排序列表中,可以将这个元素加入列表中了。如果忘记了思路或是看不懂了,更详细的解析也可以看这里,写的非常清楚。
代码和之前的基本相同。
入度表法
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
##迭代方法
adjacent = [[] for i in range(numCourses)]
indegree = [0] * numCourses
result = []
for cur,pre in prerequisites:
indegree[cur] += 1
adjacent[pre].append(cur)
from collections import deque
queue = deque()
for i in range(numCourses):
if not indegree[i]:
queue.append(i)
result.append(i)
while queue:
#element = queue.popleft()
element = queue.pop()
numCourses -= 1
for neighbor in adjacent[element]:
indegree[neighbor] -= 1
if not indegree[neighbor]:
queue.append(neighbor)
result.append(neighbor)
#print(result)
return result if numCourses == 0 else []
递归法
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
def dfs(i):
if flag[i] == -1:
return True
if flag[i] == 1:
return False
flag[i] = 1
for neighbor in adjacent[i]:
if not dfs(neighbor):
return False
flag[i] = -1
result.append(i)
return True
adjacent = [[] for _ in range(numCourses)]
flag = [0] * numCourses
result = []
for cur,pre in prerequisites:
adjacent[pre].append(cur)
for j in range(numCourses):
if not dfs(j):
return []
return result[::-1]
leetcode269 火星词典
现有一种使用字母的全新语言,这门语言的字母顺序与英语顺序不同。
假设,您并不知道其中字母之间的先后顺序。但是,会收到词典中获得一个 不为空的 单词列表。因为是从词典中获得的,所以该单词列表内的单词已经 按这门新语言的字母顺序进行了排序。
您需要根据这个输入的列表,还原出此语言中已知的字母顺序。示例 1:
输入: [ "wrt", "wrf", "er", "ett", "rftt" ] 输出: "wertf"
示例 2:
输入: [ "z", "x" ] 输出: "zx" 示例 3:
输入: [ "z", "x", "z" ]
输出: "" 解释: 此顺序是非法的,因此返回 ""。
注意:
你可以默认输入的全部都是小写字母 假如,a 的字母排列顺序优先于 b,那么在给定的词典当中 a 定先出现在 b 前面 若给定的顺序是不合法的,则返回空字符串即可 若存在多种可能的合法字母顺序,请返回其中任意一种顺序即可
从拓扑排序的角度来说,这个题其实不难,难点在于如何将词典这一问题抽象成拓扑排序。实际上,输入所反映的字母的先后顺序,也就是计算图中节点的指向顺序。
from collections import defaultdict, deque
class Solution:
def alienOrder(self, words: List[str]) -> str:
## 统计节点个数
nodes = set("".join(words))
## 建立计算图
adjacent = defaultdict(list)
indegree = dict(zip(list(nodes),[0] * len(nodes)))
for i in range(len(words) - 1):
word1,word2 = words[i],words[i + 1]
lenWord = min(len(word1),len(word2))
for j in range(lenWord):
if word1[j] != word2[j]:
adjacent[word1[j]].append(word2[j])
indegree[word2[j]] += 1
break
## 拓扑排序
result = []
queue = [i for i in indegree if indegree[i] == 0]
while queue:
element = queue.pop()
result.append(element)
for neighbor in adjacent[element]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0: queue.append(neighbor)
return "".join(result) if len(result) == len(nodes) else ""
在结果是否有效的判定中,之前的判定条件是numCourses == 0
,这里的判断条件是len(result) == len(nodes)
,其实是一样的,本质都是判断是否所有节点都被遍历到。
leetcode261 以图判树
给定从 0 到 n-1 标号的 n 个结点,和一个无向边列表(每条边以结点对来表示),请编写一个函数用来判断这些边是否能够形成一个合法有效的树结构。
示例 1:
输入: n = 5, 边列表 edges = [[0,1], [0,2], [0,3], [1,4]] 输出: true
示例 2:
输入: n = 5, 边列表 edges = [[0,1], [1,2], [2,3], [1,3], [1,4]] 输出: false
注意: 你可以假定边列表 edges 中不会出现重复的边。由于所有的边是无向边,边 [0,1] 和边 [1,0] 是相同的,因此不会同时出现在边列表 edges 中。
这个题和前面的问题有一个不同之处,就是前面的问题中,我们只需要考虑图是否成环的问题,所有的点都必然是连通的。而这个问题中,我们还要考虑是否有不被其他点连通的点存在。
其他的思路几乎相同,需要注意的就是判断条件有两个:1.所有的点都联通,这由所有的点都被遍历过来判断;2.没有环,这由边的计数来判断,和之前的numCourse对应。
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
adjacent = [[] for _ in range(n)]
for x,y in edges:
adjacent[x].append(y)
adjacent[y].append(x)
visited = set()
def helper(prev,node):
if node in visited: return False
visited.add(node)
for neighbor in adjacent[node]:
if neighbor == prev: continue
if not helper(node,neighbor): return False
return True
return helper(None,0) and len(visited) == n
另一种方法
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
##从一个点出发,能遍历所有的点,保证的是连通性
##无环性是这样保证的:从一个点出发,应该只能到达另一个点一次
##如果不重复路径可以到达两次,那就是有环
##我们记录下已经遍历过的点,如果有环,那么就会有已经遍历过的点再次出现,也就是有一条边没有被从总数中减掉
##那么就有lenEdges != 0
##这一套也可以改成递归,省个栈,改起来很容易
##最后就是注意集合比列表快得多,如果不要求顺序,还是优先用这个
adjacent = [[] for _ in range(n)]
for x,y in edges:
adjacent[x].append(y)
adjacent[y].append(x)
visited = {0}
stack = [0]
lenEdges = len(edges)
while stack:
element = stack.pop()
for neighbor in adjacent[element]:
if not neighbor in visited:
lenEdges -= 1
visited.add(neighbor)
stack.append(neighbor)
return len(visited) == n and lenEdges == 0