拓扑排序算法

拓扑排序算法

文章目录

一、前言

今天是拓扑排序算法~

二、基础知识

  • DAG图:Directed Acyclic Graph,有向无环图
  • AOV网络:Activity On Vertex Network,用顶点表示活动,用弧表示活动之间的优先关系的有向图称为顶点表示活动的网

三、拓扑排序算法

3.1 定义

拓扑序列 :设G = (V, E)是一个具有n个顶点的有向图,V中的顶点序列V1、V2、V3...Vn满足若从顶点Vi->Vj有一条路径,则在顶点序列中顶点Vi必在Vj之前。我们称其为拓扑序列

拓扑排序:对DAG图构造拓扑序列的过程

只有无环图才能产生拓扑序列

3.2 应用

  • 得到拓扑序列

  • 有向图判环

  • 关键路径(拓扑排序做中间步骤)

  • 找做事的先后顺序

  • 分层( K a h n Kahn Kahn​算法找入度为0的点就可以把层分出来)

3.3 算法

3.3.1 kahn(卡恩)算法

基本思想

基于BFS

流程

  • 在有向图中选一个没有前驱节点的顶点输出
  • 从图中删除该顶点和所有以它为尾的弧
  • 循环上述两步

具体实现

  • 建图:建图过程中维护每个顶点的入度
  • 初始:将入度为0的点全部入队
  • 队非空,则将队首顶点出队并输出,然后将该顶点的邻接点的入度减1。若邻接点入度为0,则邻接点入队
  • 队空则结束
  • 可以用一个变量计算输出顶点的个数,若全部输出则无环,否则有环

数据结构中使用的是栈,在这里是使用队列~

代码

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#include<queue>
#define LL long long
#define PII pair<int,int>
#define maxn 5005
using namespace std;
int n, m;	// n个点,m条边
// 链式前向星存图,注意边不带权值
struct edge
{
    int v, next;
    // v是邻接点,next下一条边的编号
} e[maxn];
int cnt;	// 边集数组的下标
int head[maxn];
// 入度数组
int ind[maxn];
int topo[maxn], k;
// k是下标
void add(int x,int y)
{
    cnt++;
    e[cnt].v=y;
    e[cnt].next=head[x];
    head[x]=cnt;
}
// 把环也判出来
bool kahn()
{
    int u,v;
    queue<int> q;
    for(int i=1;i<=n;i++)
    {
        if(ind[i]==0)
        {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        u=q.front();
        q.pop();
        k++;
        topo[k]=u;
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            v=e[i].v;
            ind[v]--;
            if(ind[v]==0)
            {
                q.push(v);
            }
        }
    }
    if(k==n)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}
int main()
{
    int x,y;
    memset(head,-1,sizeof(head));
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d %d",&x,&y);
        add(x,y);
        ind[y]++;// y的入度++
    }
    if(kahn()==1)
    {
        for(int i=1;i<=k;i++)
        {
            printf("%d ",topo[i]);
        }
    }
    else
    {
        printf("含环\n");
    }
    return 0;
}
/* 
输入
5 5
1 2
2 4
3 2
3 4
4 5
输出
1 3 2 4 5
*/

时间复杂度

O ( v + e ) O(v+e) O(v+e)​

链式前向星存图,遍历了所有的顶点,以及后面指向的边

3.3.2 基于DFS的算法

基本思想

使用 D F S DFS DFS算法遍历图,并且在回溯的时候将遍历的顶点入栈,那么先入栈的顶点必定入度不为0,而入度为0的顶点必定是最后入栈。最后将栈自顶向下输出,即为拓扑序列。

  • D F S DFS DFS和 B F S BFS BFS跑出来的序列不唯一,所以拓扑序列也不唯一
  • 栈可以模拟递归,但在这里不是,只是为了存储

时间复杂度

O ( v + e ) O(v+e) O(v+e)

如何判环

对于某个节点,在搜索过程中给与三种状态:0 1 2

有环:一个顶点会访问两次

具体流程

  • 在每一轮的搜索开始时,我们任取一个[未搜索]的节点开始进行深度优先搜索。当搜索到当前节点u时,我们将其置为[搜索中],遍历其相邻节点v。
  • 如果v为[未搜索],我们开始搜索v,直到搜索回溯到u
  • 如果v为[搜索中],说明存在环,不存在拓扑序列
  • 如果v为[已完成],说明v已经在栈中了,对结果不影响,不用进行任何操作

当所有的节点都被搜索完成后,如果没有找到环,那么栈中存储的节点,从栈顶到栈底的顺序即为拓扑序列

为何用DFS找拓扑序列

基于这些顶点在拓扑序列中的相对位置和在 D F S DFS DFS中的相对位置是一样的

代码

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#include<stack>
#define LL long long
#define PII pair<int,int>
#define maxn 5005
using namespace std;
int n,m;	// n个点,m条边
// 链式前向星存图,注意边不带权值
struct edge
{
    int v,next;
    // v是邻接点,next下一条边的编号
}e[maxn];
int cnt;	// 边集数组的下标->边的数目
int head[maxn];
stack<int> s;
int vis[maxn];	// 状态数组
int flag=0;// 无环
void add(int x,int y)
{
    cnt++;
    e[cnt].v=y;
    e[cnt].next=head[x];
    head[x]=cnt;
}
void dfs(int u)
{
    vis[u]=1;// u处于搜索中
    int v;
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        v=e[i].v;
        if(vis[v]==1)	// 访问过v了,但它没有入栈,说明有环
        {
            flag=1;		// 有环
            return;
        }
        else if(vis[v]==0)
        {
            dfs(v);
            if(flag==1)	// 判断dfs的过程是否有环
            {
                return;
            }	// 进行剪枝
        }
    }
    vis[u]=2;
    s.push(u);
//  return;
}
int main()
{
    int x,y;
    memset(head,-1,sizeof(head));
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d %d",&x,&y);
        add(x,y);
    }
    for(int i=0;i<=n;i++)
    {
        if(vid[i]==0)
        {
            dfs(i);
        }
    }
    if(flag==0)
    {
        while(!s.empty())
        {
            printf("%d ",s.top());
            s.pop();
        }
    }
    else
    {
        printf("含有环\n");
    }
    return 0;
}
/* 
输入
5 5
1 2
2 4
3 2
3 4
4 5
输出
3 1 2 4 5
*/

任意点开始进行遍历都没问题,因为正着找的序列不是拓扑序列,回退的过程才是拓扑序列~

3.4 题目

3.4.1 洛谷
  • P1113 [USACO02FEB] 杂务
  • P1983 [NOIP 2013 普及组] 车站分级

四、小结

关于算法的介绍就到这里啦~ 快去刷题吧~(PS:这种算法的题目往往比较难懂)

相关推荐
m0_518019482 小时前
高性能日志库C++实现
开发语言·c++·算法
UnicornDev2 小时前
从零开始的C++编程之旅——第六篇:数组与字符串——批量数据的存储与处理
java·开发语言·算法
小陈工2 小时前
2026年3月23日技术资讯洞察:AI Agent失控,Claude Code引领AI编程新趋势
开发语言·数据库·人工智能·后端·python·性能优化·ai编程
liulilittle2 小时前
LINUX RING BUFFER TUN/TAP 2
linux·运维·服务器·开发语言·网络·c++
阿里嘎多哈基米2 小时前
速通Hot100-Day10——二叉树
算法·leetcode·二叉树·递归·平衡二叉树
chushiyunen2 小时前
BM25稀疏检索算法笔记
笔记·算法·c#
芸忻2 小时前
day 23 第七章 回溯算法part02代码随想录算法训练营71期
算法
妙蛙种子3112 小时前
【Java八股 |JUC并发编程类】线程
java·开发语言·后端·多线程·八股
qq_334903152 小时前
C++中的装饰器模式高级应用
开发语言·c++·算法