跳转至

拓扑排序

约 1019 个字 157 行代码 1 张图片 预计阅读时间 7 分钟

拓扑排序

给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y之前,则称 A是该图的一个拓扑序列。

输入格式

第一行包含两个整数 n和 m。

接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。

输出格式

共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。

否则输出 −1。

数据范围

1≤n,m≤1e5

输入样例:

3 3

1 2

2 3

1 3

输出样例:

1 2 3

思路:

AC代码

C++
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;

//邻接表表示方法
int h[N],e[N*2],ne[N*2],idx;

int d[N*2];
int q[N*2];//定义一个队列
int n,m;

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

bool topsort()
{
    int hh=0,tt=-1;
    for(int i=1;i<=n;i++)
        if(!d[i]) q[++tt]=i;//将入度为0的点入队
    while(hh<=tt)
    {
        auto t=q[hh++];
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            d[j]--;
            if(d[j]==0) q[++tt]=j;
        }
    }
    return tt==n-1;//如果队列里面有n个点,则存在拓扑序列,否则有环,不存在拓扑序列
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
        d[b]++;//b的入度++
    }
    if(topsort())
    {
        for(int i=0;i<n;i++)
            cout<<q[i]<<" ";
        puts("");
    }
    else puts("-1");
    return 0;
}

拓扑排序stl实现

C++
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
using namespace std;
const int N=1e6+10;

// 邻接表存储图
int h[N], e[N*2], ne[N*2], idx; // h: 头节点,e: 目标点,ne: 下一条边,idx: 记录边序号
int d[N*2]; // 记录入度
queue<int> q;
int n, m; // 节点数,边数

// 添加边 a -> b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++; // 建立邻接表
}

// 拓扑排序(Kahn 算法)
bool topsort()
{
    for(int i = 1; i <= n; i++)
        if(!d[i]) q.push(i); // 先将入度为 0 的点入队

    int cnt = 0; // 统计拓扑序列中的节点数
    while(!q.empty())
    {
        int t = q.front(); q.pop(); // 取出队头
        for(int i = h[t]; i != -1; i = ne[i]) // 遍历 t 的所有出边
        {
            int j = e[i]; // 目标节点
            d[j]--; // 入度减少
            if(d[j] == 0) q.push(j); // 入度变 0,入队
        }
        cnt++; // 统计遍历的点数
    }
    return cnt == n; // 如果遍历所有点,说明无环,否则有环
}

int main()
{
    memset(h, -1, sizeof h); // 初始化邻接表(所有点无边)

    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b); // 添加有向边 a -> b
        d[b]++; // b 的入度 +1
    }

    if(topsort()) puts("1"); // 无环
    else puts("-1"); // 有环
    return 0;
}

详解:

拓扑排序的概念

拓扑排序(Topological Sorting)是一种用于**有向无环图(DAG,Directed Acyclic Graph)**的排序方法。它将图中的所有顶点排成一个线性序列,使得对于图中的每一条有向边 (u, v)​,u​ 都排在 v​ 之前。

拓扑排序的用途

拓扑排序常用于:

  1. 依赖关系处理

    • 任务调度(如课程安排,先学基础课再学进阶课)。
    • 编译依赖(如文件编译顺序)。
    • 解决数据流或事件流中的因果关系问题
      2. 检测有向图是否有环

    • 如果一个图的所有点都能拓扑排序,则说明图是DAG(无环图)

    • 否则,说明图中存在环,无法进行拓扑排序。

代码解析

你的代码使用邻接表存储图,并通过入度统计+队列的方法进行拓扑排序。

1. 邻接表的存储

C++
const int N=1e6+10;
int h[N],e[N*2],ne[N*2],idx;
  • h[i]​:表示顶点 i的邻接表头结点(初始值设为 -1​)。
  • e[idx]​:存储边的终点
  • ne[idx]​:存储下一个邻接点的索引,形成链表
  • idx​:维护当前边的编号。

2. 添加有向边

C++
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

这部分代码实现邻接表的构建

  • e[idx] = b​:存储边 a → b​ 终点 b​。
  • ne[idx] = h[a]​:将当前边加入 a​ 的邻接表。
  • h[a] = idx++​:更新 h[a]​ 使其指向最新加入的边。

3. 拓扑排序的实现

C++
bool topsort()
{
    int hh=0,tt=-1;
    for(int i=1;i<=n;i++)
        if(!d[i]) q[++tt]=i; // 入度为0的点入队
  • d[i]​ 记录顶点 i的入度(即有多少条边指向 i​)。
  • 遍历 1 ~ n​,将所有入度为 0的点入队列 q​。
C++
while(hh<=tt)
{
    auto t=q[hh++];
    for(int i=h[t];i!=-1;i=ne[i])
    {
        int j=e[i];
        d[j]--;
        if(d[j]==0) q[++tt]=j; // 入度变为0则入队
    }
}
  • 从队列取出一个点 t​,表示已将 t放入拓扑序列
  • 遍历 t​ 的所有邻接点 j​,入度 d[j]-- ​。
  • d[j] == 0,说明 j没有前置依赖,加入队列
C++
return tt==n-1; // 如果队列里有n个点,说明无环
  • 如果所有 n个点都被加入队列,则存在拓扑序列,返回 true​。
  • 如果有环,则 tt < n-1​,返回 false​。

4. 代码主函数

C++
int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
        d[b]++;// b 的入度 ++
    }
  • 先将 h​ 数组初始化为 -1​,表示所有点的邻接表为空
  • 读取 m​ 条有向边,并建立邻接表,同时更新 b​ 的入度。
C++
if(topsort())
{
    for(int i=0;i<n;i++)
        cout<<q[i]<<" ";
    puts("");
}
else puts("-1");
  • 如果拓扑排序成功,输出 q​(即拓扑序列)。
  • 否则输出 -1​,表示图中有环,无法拓扑排序。

运行示例

示例1:无环

输入

Text Only
5 4
1 2
1 3
3 4
2 5

图的结构

Text Only
    1 → 2 → 5
    └→ 3 → 4

输出

Text Only
1 3 2 4 5

说明可以按 1 → 3 → 2 → 4 → 5​ 这样的顺序完成拓扑排序。


示例2:有环

输入

Text Only
3 3
1 2
2 3
3 1

图的结构

Text Only
    1 → 2 → 3 → 1(形成环)

输出

Text Only
-1

因为 1 → 2 → 3 → 1​ 形成了,所以无法进行拓扑排序


总结

  • 拓扑排序的时间复杂度O(n + m)​,适用于大规模 DAG 的排序问题。
  • 队列法(Kahn算法) :使用入度统计 + BFS,适用于判断环、任务调度、依赖管理等问题。
  • 代码逻辑
  1. 初始化入度,找出所有 d[i] = 0​ 的点入队。
  2. 循环出队,依次减少相邻点的入度,若 d[j] = 0​ 再入队。
  3. 若能遍历完 n个点,说明无环;否则有环