欢迎光临
我们一直在努力

【图论】拓扑排序| 算法详解 从模板到字典序最小拓扑

拓扑排序

越是执着于这默契 越是不见底地犹豫

越是想要不着痕迹 越是怕激起涟漪

越是忽略点点滴滴 越是数不尽

找不到代替品

盼望黑夜的来临 能拥有自己

咳咳 上次写的拓扑排序太过潦草,看着有点惨不忍睹,来重新写一下,真是早知如此何必当初呢

文章目录

  • 拓扑排序
    • 概念
    • 拓扑排序的用途
    • 核心思想
    • 图存储方式
    • 考点总结
    • 模板题
    • 例题:闯关游戏

有向无环图 (DAG),对图中节点排序,满足:若存在一条边u->v 则在序列里面 u 一定出现在 v 的前面。拓扑排序只处理有向图,无向图不能拓扑排序

如果图存在环,则拓扑排序不存在。

概念

  • 有向无环图 DAG:带方向的边,图中不存在环路,才可以做拓扑排序。有环图无法生成拓扑序列。
  • 入度 ind[x]:有多少条边指向点 x。代表完成 x 之前,需要先完成多少个前置任务。
  • 拓扑排序本质:求解任务先后顺序
    • 边 u->v :代表 u 必须在 v 前面执行。
  • 拓扑排序的用途

    • 判断有向图是否存在环;

    • 输出合法的任务先后顺序;

    • DAG 上 DP,求解最长路、最短路。

    核心思想

  • 统计每个点的入度。
  • 将所有入度 = 0 的点放入容器;入度为 0 代表没有前置条件,可以直接选取。
  • 不断取出容器中的节点 x,存入答案序列;
  • 遍历 x 的所有出边 x->y,相当于完成任务x,删除边x->y:ind[y]–。
  • 如果点y入度变为 0,代表全部前置任务完成,将y放入容器。
  • 结束统计已输出节点数量 cnt:
    • cnt == n:全部节点输出完毕,DAG 无环,ans 数组即为拓扑序列。
    • cnt < n:剩余节点入度永远无法归零,图存在环,无拓扑序。
  • 普通队列 vs 优先队列(最小堆)

    • queue普通队列:输出任意一种合法拓扑序。
    • priority_queue<ll,vector<ll>,greater<ll>>最小堆:每次选编号最小的点,输出字典序最小拓扑序列
    • 如果不需要字典序最小,只需要任意合法拓扑序,把优先队列替换普通队列
    容器用途时间复杂度
    queue普通队列 输出任意合法拓扑序,大数据首选 O(n+m)
    小根堆 greater<ll> 输出字典序最小拓扑序列 O(nlogn+m)
    默认大根堆 输出字典序最大拓扑序列 O(nlogn+m)

    图存储方式

    邻接表:vector<vector<ll>> adj(n+1)

    ​ adj[u]保存 u 可以直接到达的所有节点。

    入度数组:vector<ll> ind(n+1,0),记录每个节点入度。

    考点总结

    • 判断有向图是否存在环:ans.size() != n / cnt != n,说明存在环,没有拓扑序列。
    • 字典序最小拓扑序:使用小根堆 priority_queue<ll,vector<ll>,greater<ll>>。
    • 字典序最大拓扑序:使用大根堆(默认优先队列,不加 greater)。
    • DAG 上 DP:按拓扑序遍历节点,求 DAG 最长路 / 最短路。保证处理当前点时,它所有前驱节点已经全部算完。

    模板题

    任意合法序列

    【模板】拓扑排序

    #include<bits/stdc++.h>
    #define ll long long
    #define endl '\\n'
    #define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    #define ull unsigned long long
    #define fi first
    #define se second
    #define PLL pair<ll, ll>
    #define YES cout<<"YES"<<endl;
    #define NO cout<<"NO"<<endl;
    using namespace std;
    const ll N=2e5+10;
    int main()
    {
    IOS
    int n,m;
    cin>>n>>m;
    vector<vector<int>> adj(n+1);
    vector<int> ind(n+1,0);
    for(int i=1;i<=m;i++)
    {
    int u,v;
    cin>>u>>v;
    adj[u].push_back(v);
    ind[v]++;
    }
    queue<int> q;
    for(int i=1;i<=n;i++)
    {
    if(ind[i]==0)
    q.push(i);
    }
    vector<int> ans;
    while(!q.empty())
    {
    int x=q.front();
    q.pop();
    ans.push_back(x);
    for(auto y:adj[x])
    {
    ind[y];
    if(ind[y]==0) q.push(y);
    }
    }
    if(ans.size()!=n)
    {
    cout<<1<<endl;
    }
    else
    {
    for(int i=0;i<ans.size();i++)
    {
    if(i)
    cout<<" ";
    cout<<ans[i];
    }
    cout<<endl;
    }
    return 0;
    }

    例题:闯关游戏

    就像这道题目中表明了 要先输出字典序最小的闯关序列

    F-闯关游戏_河南萌新联赛2026第(一)场:河南工业大学

    在这里插入图片描述

    //F
    #include<bits/stdc++.h>
    #define ll long long
    #define endl '\\n'
    #define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    #define ull unsigned long long
    #define fi first
    #define se second
    using namespace std;
    int main()
    {
    IOS
    ll t;
    cin>>t;
    while(t)
    {
    ll n,m;
    cin>>n>>m;
    vector<vector<ll>>adj(n+1);
    vector<ll>ind(n+1,0);
    for (int i=1;i<=m;i++)
    {
    ll u,v;
    cin>>u>>v;
    adj[u].push_back(v);//记录线路
    ind[v]++;//记录每个顶点的度
    }
    priority_queue<ll,vector<ll>,greater<ll>> q;
    for(int i=1;i<=n;i++)
    {
    if(ind[i]==0)
    {
    q.push(i);//用队列来遍历度为0的边
    }
    }
    ll cnt=0;//用来记录删去点的请况以观察是否成环
    vector<ll>ans;
    while(!q.empty())
    {
    ll x=q.top();
    q.pop();
    ans.push_back(x);
    cnt++;
    for(auto it:adj[x])
    {
    ind[it];
    if(ind[it]==0)
    {
    q.push(it);
    }
    }
    }
    if(cnt!=n)
    {
    cout<<"No"<<endl;
    }
    else
    {
    cout<<"Yes"<<endl;
    for(int k=0;k<ans.size();k++)
    {
    cout<<ans[k]<<" ";
    }
    cout<<endl;
    }
    }

    //cout<<fixed<<setprecision(x)<< ;
    return 0;
    }

    赞(0)
    未经允许不得转载:171主机测评 » 【图论】拓扑排序| 算法详解 从模板到字典序最小拓扑
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址