拓扑排序

拓扑排序

拓扑排序的概念

拓扑排序(Topological sorting)要解决的问题是如何给一个有向无环图的所有节点排序。因此我们可以说 在一个 DAG(有向无环图) 中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 \(u\) 到 \(v\) 的有向边 \((u,v)\), 都有 \(u\) 在 \(v\) 的前面。给定一个 DAG,如果从 \(i\) 到 \(j\) 有边,则认为 \(j\) 依赖于 \(i\)。如果 \(i\) 到 \(j\) 有路径(\(i\) 可达 \(j\)),则称 \(j\) 间接依赖于 \(i\)。

\({ \color{red}\text{拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。}}\)

拓扑排序的 dfs 实现

DFS 总是沿着一条路搜索到无出度的节点,然后逐层回退,因此 DFS 适合拓扑排序,并且我们可以在回退的过程中保存下拓扑排序的逆序。

从所有零入度的节点开始,就能到达所有点;也可以从未被遍历到的节点开始。

\({\color{red}\text{注意:要仔细看定义。}}\)

cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

const int N = 1e5 + 10;
vector< int > G[N];
// 1代表正在访问,-1代表访问结束,0代表未访问
int vis[N];
int ans[N], tot;
int innode[N];

bool dfs_topsort(int u)  // 从节点u出发
{
    vis[u] = 1;  // 正在访问
    for (int i = 0; i < G[u].size(); i++)
    {
        int v = G[u][i];
        // 如果后继比前驱先访问,说明存在有向环
        if (vis[v] == 1)
        {
            return false;
        }
        // 如果后继未被访问
        if (!vis[v])
        {
            // 访问后继返回假,也是失败
            if (!dfs_topsort(v))
            {
                return false;
            }
        }
    }
    vis[u] = -1;
    // 在递归结束才加入拓扑序列数组中,
    // 最深层次先返回
    ans[tot++] = u;
    return true;
}

int main()
{
    int n, m, a, b;
    cin >> n >> m;
    while (m--)
    {
        cin >> a >> b;
        G[a].push_back(b); // 邻接数组存图
        innode[b]++; // 纪录入度
    }
    for (int i = 1; i <= n; i++)
    {
        if (innode[i] == 0)
        {
            dfs_topsort(i); // 拓扑排序
        }
    }
    for (int i = tot - 1; i >= 0; i--)
    {
        cout << ans[i] << " "; // 输出答案
    }
    return 0;
}

拓扑排序的 bfs 实现

  1. 在有向图中选一个无前驱的顶点并且输出。

  2. 从图中删除所有和它有关的边。

  3. 重复上述两步,直至所有顶点输出。如果还有顶点未输出,则说明有环。

cpp 复制代码
#include <iostream>
#include <queue>
#include <vector>

using namespace std;

const int N = 1e5 + 10;
queue< int > q;
vector< int > G[N];
vector< int > ans;  // 存放拓扑序列
int in[N];
bool bfs_topsort(int n)  // n为节点个数
{
    for (int i = 1; i <= n; i++)
    {
        if (in[i] == 0)
        {
            q.push(i);  // 将入度为0的点入队列
        }
    }
    while (!q.empty())
    {
        int u = q.front();
        q.pop();  // 选一个入度为 0 的点,出队
        ans.push_back(u);
        for (int i = 0; i < G[u].size(); i++)
        {
            int v = G[u][i];
            in[v]--;
            if (in[v] == 0)
            {
                // 当某一条边的入度为0,入队
                q.push(v);
            }
        }
    }
    if (ans.size() == n)
    {
        for (int i = 0; i < ans.size(); i++)
        {
            // 输出答案
            cout << ans[i] << " ";
        }
        return true;
    }
    return false;  // 图内还有剩余节点,存在环路
}

int main()
{
    int n;
    int m;
    int a;
    int b;
    cin >> n >> m;
    while (m--)
    {
        cin >> a >> b;
        G[a].push_back(b);  // 邻接数组存图
        in[b]++; // 纪录入度
    }
    if (!bfs_topsort(n))
    {
        cout << "No answer";
    }
    cout << endl;
    return 0;
}

注意:如果需要输出和字典序相关的拓扑序列,那么只要把队列替换为优先队列即可。

参考文献

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

https://blog.csdn.net/qq_44691917/article/details/104198637