拓扑排序与有向无环图
文章目录
1. 什么是拓扑排序
核心思想:
根据节点之间的依赖关系进行排序
拓扑排序并非等同于快排等常见算法的应用场景,下面对其进行对比以便明晰拓扑排序的使用场景和排序目的:
快速排序(Quick Sort)
- 目的:对一组数按大小顺序进行排序。
- 应用场景:用于对无序数组进行排序,使得数组中的元素按升序或降序排列。
- 算法思想:通过选择一个基准元素,将数组分成两部分,一部分小于基准元素,另一部分大于基准元素,然后递归地对这两部分进行排序。
拓扑排序(Topological Sort)
- 目的:对有向无环图(DAG)中的节点进行排序,使得对于每一条有向边 ( u -> v ),节点 ( u ) 在排序结果中出现在节点 ( v ) 之前。
- 应用场景:用于表示依赖关系的场景,例如任务调度、课程安排、编译顺序等。
- 算法思想:通过不断移除入度为0的节点,构建一个线性序列,使得每个节点都在其所有前驱节点之后。
主要区别
- 数据结构:快排处理的是数组或列表,而拓扑排序处理的是有向无环图。
- 排序依据:快排根据元素的大小进行排序,而拓扑排序根据图中的依赖关系进行排序。
- 结果:快排的结果是一个按大小顺序排列的数组,而拓扑排序的结果是一个满足依赖关系的节点序列。
2. 拓扑排序与有向无环图之间的契合性
- 依赖关系:在有向无环图中,每条有向边 ( u -> v ) 表示节点 ( u ) 是节点 ( v ) 的前驱,或者说 ( v ) 依赖于 ( u )。拓扑排序的目标是找到一个线性序列,使得每个节点都在其所有前驱节点之后。
- 无环性:如果图中存在环,那么就无法找到一个满足所有依赖关系的线性序列。例如,如果存在一个环 ( u -> v -> w ->u ),那么 ( u ) 既需要在 ( v ) 之前,又需要在 ( v ) 之后,这是不可能的。
3. Kahn算法实现拓扑排序
算法思想
Kahn算法的核心思想是通过不断移除入度为0的节点来实现拓扑排序。核心步骤如下:
- 入度为0的节点:在有向无环图(DAG)中,入度为0的节点是没有任何前驱节点的节点。这些节点可以作为拓扑排序的起点。
- 移除节点:将入度为0的节点从图中移除,并将其加入拓扑排序结果中。然后,更新其所有邻接节点的入度。
- 重复过程:重复上述过程,直到所有节点都被移除。如果在某个时刻没有入度为0的节点,但仍有节点未被移除,则说明图中存在环,无法进行拓扑排序。
算法步骤
- 读取输入:
- 读取点的个数 ( n ) 和边的条数 ( m )。
- 初始化一个邻接表
graph
和一个入度数组in_degree
,用于存储每个节点的入度。
- 构建图和入度数组:
- 读取每条边的信息,并更新邻接表和入度数组。
- 邻接表
graph[u]
存储从节点 ( u ) 出发的所有边的终点。 - 入度数组
in_degree[v]
记录节点 ( v ) 的入度,即有多少条边指向 ( v )。
- 初始化队列:
- 创建一个队列
q
,用于存储所有入度为0的节点。 - 遍历所有节点,将入度为0的节点加入队列。
- 创建一个队列
- 拓扑排序:
- 创建一个向量
topo_order
,用于存储拓扑排序结果。 - 当队列不为空时,执行以下操作:
- 从队列中取出一个节点
node
,将其加入topo_order
。 - 遍历
node
的所有邻接节点neighbor
,将这些邻接节点的入度减1。 - 如果某个邻接节点的入度变为0,则将其加入队列。
- 从队列中取出一个节点
- 创建一个向量
- 检查结果:
- 如果
topo_order
的大小等于节点数 ( n ),则输出拓扑排序结果。 - 否则,输出 -1,表示图中存在环,无法进行拓扑排序。
- 如果
算法代码
// 假定有如下题目:
/*
给定一个包含 n 个点 m 条边的有向无环图,求出该图的拓扑序。若图的拓扑序不唯一,输出任意合法的拓扑序即可。若该图不能拓扑排序,输出-1。
输入描述:
第一行输入两个整数 n,m(1≤n,m<2·10^5),表示点的个数和边的条数。
接下来的 m 行,每行输入两个整数 Ui,Vi(1≤u,v≤n),表示 Ui 到 Vi 之间有一条有向边。
输出描述:
若图存在拓扑序,输出一行 n 个整数,表示拓扑序。否则输出 -1。
*/
#include <iostream>
#include <vector>
#include <queue>
int main() {
int n, m;
std::cin >> n >> m;
std::vector<std: