LogoCSP Wiki By Yundou
4 Graph

拓扑排序与Kahn算法

定义

拓扑排序(Topological sorting)要解决的问题是如何给一个有向无环图的所有节点排序。

我们可以拿大学每学期排课的例子来描述这个过程,比如学习大学课程中有:「程序设计」,「算法语言」,「高等数学」,「离散数学」,「编译技术」,「普通物理」,「数据结构」,「数据库系统」等。按照例子中的排课,当我们想要学习「数据结构」的时候,就必须先学会「离散数学」,学习完这门课后就获得了学习「编译技术」的前置条件。当然,「编译技术」还有一个更加前的课程「算法语言」。这些课程就相当于几个顶点 uu, 顶点之间的有向边 (u,v)(u,v) 就相当于学习课程的顺序。教务处安排这些课程,使得在逻辑关系符合的情况下排出课表,就是拓扑排序的过程。

图片描述

但是如果某一天排课的老师打瞌睡了,说想要学习 数据结构,还得先学 操作系统,而 操作系统 的前置课程又是 数据结构,那么到底应该先学哪一个(不考虑同时学习的情况)?在这里,数据结构 和 操作系统 间就出现了一个环,显然同学们现在没办法弄清楚自己需要先学什么了,也就没办法进行拓扑排序了。因为如果有向图中存在环路,那么我们就没办法进行拓扑排序。

因此我们可以说 在一个DAG(有向无环图)中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 uuvv 的有向边 (u,v)(u,v), 都可以有 uuvv 的前面。

还有给定一个 DAG,如果从 iijj 有边,则认为 jj 依赖于 ii。如果 iijj 有路径(ii 可达 jj),则称 jj 间接依赖于 ii

拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。

Kahn 算法

构造拓扑序列步骤

  1. 从图中选择一个入度为零的点。
  2. 输出该顶点,从图中删除此顶点及其所有的出边。

重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。

过程

初始状态下,集合 SS 装着所有入度为 00 的点,LL 是一个空列表。

每次从 SS 中取出一个点 uu(可以随便取)放入 LL, 然后将 uu 的所有边 (u,v1),(u,v2),(u,v3)(u, v_1), (u, v_2), (u, v_3) \cdots 删除。对于边 (u,v)(u, v),若将该边删除后点 vv 的入度变为 00,则将 vv 放入 SS 中。

不断重复以上过程,直到集合 SS 为空。检查图中是否存在任何边,如果有,那么这个图一定有环路,否则返回 LLLL 中顶点的顺序就是构造拓扑序列的结果。

代码的核心是维持一个入度为 0 的顶点的集合。

可以参考该图

图片描述

对其排序的结果就是:2 -> 8 -> 0 -> 3 -> 7 -> 1 -> 5 -> 6 -> 9 -> 4 -> 11 -> 10 -> 12

时间复杂度

假设这个图 G=(V,E)G = (V, E) 在初始化入度为 00 的集合 SS 的时候就需要遍历整个图,并检查每一条边,因而有 O(E+V)O(E+V) 的复杂度。然后对该集合进行操作,显然也是需要 O(E+V)O(E+V) 的时间复杂度。

因而总的时间复杂度就有 O(E+V)O(E+V)

实现

int n, m;
vector<int> G[MAXN];
int in[MAXN];  // 存储每个结点的入度
 
bool toposort() {
  vector<int> L;
  queue<int> S;
  for (int i = 1; i <= n; i++)
    if (in[i] == 0) S.push(i);
  while (!S.empty()) {
    int u = S.front();
    S.pop();
    L.push_back(u);
    for (auto v : G[u]) {
      if (--in[v] == 0) {
        S.push(v);
      }
    }
  }
  if (L.size() == n) {
    for (auto i : L) cout << i << ' ';
    return true;
  }
  return false;
}

例题:

On this page