拓扑排序的c++实现

 
Category: DSA

写在前面

写一下有向无环图(DAG, Directed Acyclic Graph)上的拓扑排序, 废话不多说了, 介绍部分大家可以参考算法导论或者 oi-wiki.

https://oi-wiki.org/graph/topo/;

我写的完整代码: Topological-Sort

我们可以拿大学选课的例子来描述这个过程,比如学习大学课程中有:单变量微积分,线性代数,离散数学概述,概率论与统计学概述,语言基础,算法导论,机器学习。当我们想要学习 算法导论 的时候,就必须先学会 离散数学概述 和 概率论与统计学概述,不然在课堂就会听的一脸懵逼。当然还有一个更加前的课程 单变量微积分。这些课程就相当于几个顶点 , 顶点之间的有向边 就相当于学习课程的顺序。显然拓扑排序不是那么的麻烦,不然你是如何选出合适的学习顺序。下面将介绍如何将这个过程抽象出来,用算法来实现。

但是如果某一天排课的老师打瞌睡了,说想要学习 算法导论,还得先学 机器学习,而 机器学习 的前置课程又是 算法导论,然后你就一万脸懵逼了,我到底应该先学哪一个?当然我们在这里不考虑什么同时学几个课程的情况。在这里,算法导论 和 机器学习 间就出现了一个环,显然你现在没办法弄清楚你需要学什么了,于是你也没办法进行拓扑排序了。因而如果有向图中存在环路,那么我们就没办法进行 拓扑排序 了。

思路

有两种实现方法, 一种是基于 BFS 的, 叫做 Kahn 算法, 本质上就是广度优先搜索, 统计入度为 0 的所有节点, 放入结果集合, 同时将入度为 1 的放入结果集合, 最后判断结果集大小是不是结点个数(这样才说明图上每一个节点都遍历到了, 也即图是 DAG).

还有一种是基于深度优先搜索的, 在 DFS 的时候, 遍历直到节点没有出度(即到达函数结尾, return 之前), 此时的节点就是”末尾”节点, 也就是说一条深搜路径的结尾, 这时候放入栈中, 结果集就是栈的出栈序列.

实现: BFS

bool topoSort_Kahn() {
    vector<int> L;
    queue<int> S;
    // 计算节点入度
    for (auto v : G)
        for (int i : v) 
            ++in[i];
    // 存入度为 0 的节点
    for (int i{}; i < n; ++i)
        if (in[i] == 0) S.push(i); 
    // BFS
    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) {
        cout << "排序结果:\n" << L;
        return true;
    }
    return false;
}

实现: DFS

void topoSort_DFS() {
    //
    bool visited[n];
    memset(visited, false, sizeof(visited));
    stack<int> st;

    function<void(int)> dfs = [&](int v) {
        visited[v] = true;
        for (auto i : G[v])
            if (!visited[i]) dfs(i);
        st.emplace(v);
    };
    for (int i{}; i < n; ++i)
        if (!visited[i]) dfs(i);
    cout << st;
}

代码示例

utility-function

#include <iostream>
#include <vector>
#include <queue>
#include <stack>
using namespace std;


ostream& operator<<(ostream& os, const vector<int>& v) {
    for (auto i : v) os << i << " ";
    return os << endl;
}
ostream& operator<<(ostream& os, queue<int> v) { // pass by value
    for (; !v.empty(); v.pop()) os << v.front() << " ";
    return os << endl;
}
ostream& operator<<(ostream& os, stack<int> v) { // pass by value
    for (; !v.empty(); v.pop()) os << v.top() << " ";
    return os << endl;
}

针对上面这个图, 采用不同的拓扑排序策略, 得到的排序结果不同:

// 图的例子: 参见
// https://oi-wiki.org/graph/images/topo-example.svg
// 邻接表
vector<vector<int>> G{
    {1, 5, 6}, {},  {0, 3},       {5}, {},   {4}, {4, 9},
    {6},       {7}, {10, 11, 12}, {},  {12}, {},
};

int n = G.size(); // n: vertex
// int m{15};        // m: edge
// 存储结点的入度
vector<int> in(n);
// 1, 1, 0, 1, 2, 2, 2, 1, 0, 1, 1, 1, 2

int main() {
    // topoSort_Kahn(); // BFS
    // 2 8 0 3 7 1 5 6 4 9 10 11 12
    topoSort_DFS();
    // 8 7 2 3 0 6 9 11 12 10 5 4 1
}