拓扑排序(算法基础)
拓扑排序
试想这样一个问题,我们做事情事情本身之间有先决条件。例如我们希望能在这些事情之间找到不破坏依赖关系的前提下对整个结果排序。这被称为拓扑排序。假设下面的例子,每个节点称为一个课程。
我们学习C3则需要先学C2和C1。也就是说我们应该先学没有任何依赖的课程(没有被指向的节点)。于是我们定义节点的入度表示有多少指向其的节点。例如C3的入度为2,因为有两个节点C1和C2指向它。
C3课程需要C1和C2先修课程,如果C1当前已经完成,此时想上C3则需要完成剩下的课程C2。当然我们也可以先完成C2再完成C1,也就是说拓扑排序的结果不一定是唯一的。我们上了某门课则需要将次课程指向的课程入度-1(或者也可以说将指向的边删除C1–>C3的边删除)。于是我们可以简单地总结拓扑排序:
- 找到图中入度为0的点加入结果
- 将节点关联的其它节点的入度-1
- 在剩下节点中找到入度为0的节点重复操作1,2直到最后所有的节点入度都为0为止。
算法分析
根据上面的算法我们需要知道:
- 首先知道每个节点的入度值。
- 根据节点找到其指向的节点。
- 快速找到入度为0的节点。
算法流程分析
入度表:
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 2 | 2 | 1 | 2 | 2 | 1 | 1 |
为了能顺序访问,我们将入度为0的节点放入队列。算法执行:
- 将入度为0的C1、C2加入队列
- 将队列头的C1取出,根据C1找到其相连的C3和C8节点,然后将C3和C8的入度-1。此时图如下:
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 2 | 1 | 2 | 2 | 0 | 1 |
这时我们发现C8入度为0,加入队列。此时队列中元素为[C2,C8]
3. C2出队列,将C3和C5的入度-1。
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 2 | 0 | 2 | 2 | 0 | 1 |
此时C3和C5已经入度为0,入队列,此时队列中元素[C8,C3,C5]
4. C8出队列,然后C9入度-1
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 2 | 0 | 2 | 2 | 0 | 0 |
此时C9入度为0,将C9加入队列。此时队列中[C3,C5,C9]
5. C3出队列,C4入度-1
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 2 | 2 | 0 | 0 |
此时没有节点入度为0,则不需加入任何节点到队列中。队列中元素[C5,C9]
6. C5出队列,C4和C6入度-1
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 1 | 2 | 0 | 0 |
此时C4入度为0,将C4加入队列。队列中元素[C9,C4]
7. C9出队列,C7入度-1。
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
此时队列中元素为C4
8. C4出队列,C6、C7入度-1。
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
此时C6和C7入度都为0,将其加入队列,此时队列元素[C6,C7]
9.最后C6、C7出队列。队列为空,则可以拓扑排序。
什么样的情况无法拓扑排序?
此时没有节点入度为0。核心在于C4课程依赖于C7,而C7同样依赖于C4。
实践
def canFinish(numCourses: int, prerequisites: List[List[int]]) -> bool:
node_link_nodes = {}
node_indgree = {}
indegree = set()
for node in prerequisites:
if node[1] not in node_indgree:
node_indgree[node[1]] = 1
else:
node_indgree[node[1]] += 1
# 入度不为0的节点加入indegree
indegree.add(node[1])
if node[0] not in node_link_nodes:
node_link_nodes[node[0]] = [node[1]]
else:
node_link_nodes[node[0]].append(node[1])
nodes = deque(set(range(numCourses)) - indegree)
out = []
while len(nodes) > 0:
no_indegree = nodes.popleft()
out.append(no_indegree)
if no_indegree in node_link_nodes:
for node in node_link_nodes[no_indegree]:
node_indgree[node] -= 1
if node_indgree[node] == 0:
nodes.append(node)
return len(out) == numCourses
def findOrder(numCourses: int, prerequisites: List[List[int]]) -> List[int]:
indegree = [0] * numCourses
# 节点和其连接的相邻节点
adj = {}
res = []
for edge in prerequisites:
indegree[edge[0]] += 1
for x, y in prerequisites:
if y not in adj:
adj[y] = []
adj[y].append(x)
visited = [False for i in range(numCourses)]
node_indeies = []
for node_index, value in enumerate(indegree):
if value == 0 and not visited[node_index]:
node_indeies.append(node_index)
visited[node_index] = True
while len(node_indeies) > 0:
joined_node = node_indeies.pop(0)
res.append(joined_node)
if joined_node in adj:
for related_index in adj[joined_node]:
indegree[related_index] -= 1
if indegree[related_index] == 0 and not visited[related_index]:
node_indeies.append(related_index)
visited[related_index] = True
if joined_node in adj:
adj.pop(joined_node)
return res if len(res) == numCourses else []