还不会拓扑排序?看这一篇就够了

这篇具有很好参考价值的文章主要介绍了还不会拓扑排序?看这一篇就够了。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、什么是拓扑排序?

拓扑排序是一种有向无环图(DAG)的顶点排序方法,它将一个有向无环图中的所有顶点排成一个线性序列,使得图中任意一条有向边上的起点排在终点的前面

这样说还不够具体,我们先来看一个例子。假设某大学的课程安排如下:

课程编号 课程名称 先修课程
1 1 1 高等数学 − -
2 2 2 程序设计基础 − -
3 3 3 离散数学 1 ,   2 1,\,2 1,2
4 4 4 数据结构 2 ,   3 2,\,3 2,3
5 5 5 高级语言程序设计 2 2 2
6 6 6 编译方法 4 ,   5 4,\,5 4,5
7 7 7 操作系统 4 ,   9 4,\,9 4,9
8 8 8 普通物理 1 1 1
9 9 9 计算机原理 8 8 8

为了顺利修完这九门课程,我们必须安排一个合理的学习顺序。

首先根据以上表格构建一个有向图,即若 j j j 的先修课程有 i i i,则画一条 i i i j j j 的有向边,于是可以得到:

还不会拓扑排序?看这一篇就够了

一个合理的学习顺序是: 1 → 8 → 9 → 2 → 3 → 5 → 4 → 7 → 6 1\to8\to9\to2\to3\to5\to4\to7\to6 189235476,该序列又称拓扑序列,是对上述有向图进行拓扑排序后的结果。

可以看出,拓扑序列不唯一。例如, 1 → 8 → 9 → 2 → 3 → 5 → 4 → 6 → 7 1\to8\to9\to2\to3\to5\to4\to6\to7 189235467 也是满足要求的学习顺序。

此外还可以证明,DAG一定存在拓扑序列,存在拓扑序列的图也一定是DAG,因此DAG又被称为拓扑图

二、拓扑排序的实现

拓扑排序的具体步骤如下:

  • 找到所有入度为 0 0 0 的顶点,并将其输出到拓扑序列中。
  • 将这些顶点从图中删除,并将所有以该顶点为起点的边的终点的入度减 1 1 1
  • 不断重复以上两个操作,直到所有的顶点都被输出到拓扑序列中或者图中不存在入度为 0 0 0 的顶点为止。

代码实现:

int n, m;  // n表示节点数量,m表示边的数量
int h[N], e[N], ne[N], idx;  // 邻接表存图
int in[N];  // 保存每个点的入度
vector<int> L;  // 存储拓扑序列

// 拓扑排序算法
void topo_sort() {
    queue<int> q;
    for (int i = 1; i <= n; i++)
    	// 找到所有入度为0的点然后将其添加到队列中,同时也输出到拓扑序列中
        if (in[i] == 0) {
            q.push(i);
            L.push_back(i);
        }

    while (!q.empty()) {
        auto t = q.front();
        q.pop();

        for (int i = h[t]; ~i; i = ne[i]) {  // 遍历t的所有相邻节点
            int j = e[i];
            in[j]--;  // 将相邻节点的入度-1
            // 如果相邻节点的入度减到0,则入队列,同时将其输出到拓扑序列中
            if (in[j] == 0) {
                q.push(j);
                L.push_back(i);
            }
        }
    }
}

如果 L.size() == n 成立,说明该图存在拓扑序列,否则说明该图不是DAG。

拓扑排序的时间复杂度和BFS相同,均为 O ( n + m ) O(n+m) O(n+m)

2.1 拓扑排序模版

上述代码显得过于臃肿,我们可以进一步简化以形成模版(务必背过)。

void topo_sort() {
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);

    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        L.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (!--in[j]) q.push(j);
        }
    }
}

三、拓扑排序的应用

3.1 有向图的拓扑序列

🔗 原题链接:AcWing 848. 有向图的拓扑序列

直接套模板即可。

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], ne[N], idx;
int in[N];
vector<int> L;

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

void topo_sort() {
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);

    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        L.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (!--in[j]) q.push(j);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    memset(h, -1, sizeof h);

    cin >> n >> m;
    while (m--) {
        int x, y;
        cin >> x >> y;
        add(x, y), in[y]++;
    }

    topo_sort();

    if (L.size() == n) {
        for (auto i: L) cout << i << ' ';
        cout << "\n";
    } else cout << -1 << "\n";

    return 0;
}

3.2 家谱树

🔗 原题链接:AcWing 1191. 家谱树

如果 b b b a a a 的孩子,则画一条由 a a a 指向 b b b 的有向边,然后拓扑排序即可。

#include <bits/stdc++.h>

using namespace std;

const int N = 110, M = N * N / 2;

int n;
int h[N], e[M], ne[M], idx;
int in[N];
vector<int> L;

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

void topo_sort() {
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);

    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        L.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (!--in[j]) q.push(j);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    memset(h, -1, sizeof h);

    cin >> n;

    for (int i = 1; i <= n; i++) {
        int x;
        while (cin >> x, x) {
            add(i, x);
            in[x]++;
        }
    }

    topo_sort();

    for (auto i: L) cout << i << ' ';
    cout << "\n";

    return 0;
}

3.3 奖金

🔗 原题链接:AcWing 1192. 奖金

对于建图,如果 b b b 的奖金比 a a a 高,则画一条 a a a b b b 的有向边。

要使总奖金最少,很显然,初始时所有入度为 0 0 0 的点的奖金都为 100 100 100 元。之后,对于图中任意两点 x , y x,y x,y,如果存在 x x x y y y 的有向边,那么 y y y 的奖金应当比 x x x 的奖金多一元

我们可以让队列不止保存员工的编号,还保存每位员工的奖金,这样在遍历的时候就能够方便的去计算每位员工的奖金了。

#include <bits/stdc++.h>

using namespace std;

const int N = 10010, M = 20010;

int n, m;
int h[N], e[M], ne[M], idx;
int in[N];
vector<int> L;

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

void topo_sort() {
    queue<pair<int, int>> q;
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.emplace(i, 100);

    while (!q.empty()) {
        auto [t, bonus] = q.front();
        q.pop();
        L.push_back(bonus);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (!--in[j]) q.emplace(j, bonus + 1);  // 多一元
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    memset(h, -1, sizeof h);

    cin >> n >> m;
    while (m--) {
        int x, y;
        cin >> x >> y;
        add(y, x), in[x]++;
    }

    topo_sort();

    if (L.size() == n) cout << accumulate(L.begin(), L.end(), 0) << "\n";
    else cout << "Poor Xed\n";

    return 0;
}

3.4 可达性统计

🔗 原题链接:AcWing 164. 可达性统计

本题可以用BFS暴力求解,时间复杂度为 O ( n ( n + m ) ) ≈ 1.8 × 1 0 9 O(n(n+m))\approx1.8\times 10^9 O(n(n+m))1.8×109,只能过 3 / 5 3/5 3/5 的测试点。

不妨设从 x x x 出发能够到达的点的集合为 f ( x ) f(x) f(x),易知

f ( x ) = { x } ∪ ⋃ y ∈ N ( x ) f ( y ) f(x)=\{x\}\cup \bigcup_{y\in N(x)} f(y) f(x)={x}yN(x)f(y)

也就是说,从 x x x 出发能够到达的点的集合为从「 x x x 的各个后继节点」出发能够到达的点的集合的并集再加上 x x x 自身。所以,只有在计算出一个点的所有后继节点的 f f f 值之后,我们才能够算出该点的 f f f 值。这启发我们先对图进行拓扑排序得到一个拓扑序列,然后再倒序计算。

我们可以用 n n n 位二进制数来存储每个 f ( x ) f(x) f(x),其中第 i i i 位是 1 1 1 表示 x x x 能到 i i i,第 i i i 位是 0 0 0 表示 x x x 不能到 i i i。这样一来,对若干个集合求并就相当于对若干个二进制数进行按位或运算,每个 f ( x ) f(x) f(x) 1 1 1 的个数就代表了从 x x x 出发能够到达的节点的数量。

#include <bits/stdc++.h>

using namespace std;

const int N = 30010;

int n, m;
int h[N], e[N], ne[N], idx;
int in[N];

vector<int> L;
bitset<N> f[N];

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

void topo_sort() {
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);

    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        L.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (!--in[j]) q.push(j);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    memset(h, -1, sizeof h);

    cin >> n >> m;
    while (m--) {
        int x, y;
        cin >> x >> y;
        add(x, y), in[y]++;
    }

    topo_sort();

    for (int i = n - 1; ~i; i--) {  // 倒序遍历
        int j = L[i];
        f[j][j] = 1;  // j一定能到达自身
        for (int k = h[j]; ~k; k = ne[k])
            f[j] |= f[e[k]];  // 按照公式计算出f[j]的值
    }

    for (int i = 1; i <= n; i++) cout << f[i].count() << "\n";

    return 0;
}

3.5 Directing Edges

🔗 原题链接:CF 1385E

本题说白了就是给图中的所有无向边指定方向,使得最终得到的图是一个DAG。

我们可以先考虑仅由有向边构成的子图,如果这个子图都不是DAG,那么后续无论怎样为无向边指定方向(相当于添加有向边)都不可能构成DAG,此时应当输出 NO。如果这个子图是DAG,那么我们一定可以通过指定无向边的方向来构造出新的DAG。

具体来说,我们先对有向边构成的子图进行拓扑排序,于是可以得到一个拓扑序列。对于每一个无向边 ( a , b ) (a,b) (a,b),若 a a a 在拓扑序列中的下标小于 b b b 在拓扑序列中的下标,则添加有向边 a → b a\to b ab,否则添加有向边 b → a b\to a ba,这样可以保证添加完有向边后得到的图仍然是DAG。文章来源地址https://www.toymoban.com/news/detail-405332.html

#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 10;

int n, m;
int h[N], e[N], ne[N], idx;
int in[N];

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

void topo_sort(vector<int> &L) {
    queue<int> q;
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);

    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        L.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (!--in[j]) q.push(j);
        }
    }
}

void solve() {
    memset(h, -1, sizeof h);
    memset(in, 0, sizeof in);

    vector<int> L;
    vector<pair<int, int>> edges;

    cin >> n >> m;
    while (m--) {
        int t, x, y;
        cin >> t >> x >> y;
        if (t) add(x, y), in[y]++;
        else edges.emplace_back(x, y);
    }

    topo_sort(L);

    if (L.size() != n) cout << "NO\n";
    else {
        cout << "YES\n";
        // 先输出有向图部分
        for (int i = 1; i <= n; i++)
            for (int j = h[i]; ~j; j = ne[j])
                cout << i << ' ' << e[j] << "\n";

        vector<int> pos(n + 1);  // 因为节点的编号是从1开始的,所以要多开一个
        for (int i = 0; i < n; i++) pos[L[i]] = i;
        // 再输出补全的部分
        for (auto [a, b]: edges) {
            if (pos[a] > pos[b]) swap(a, b);
            cout << a << ' ' << b << "\n";
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;

    while (t--) {
        solve();
    }

    return 0;
}

到了这里,关于还不会拓扑排序?看这一篇就够了的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 精通线程池,看这一篇就够了

    当我们运用多线程技术处理任务时,需要不断通过new的方式创建线程,这样频繁创建和销毁线程,会造成cpu消耗过多。那么有没有什么办法 避免频繁创建线程 呢? 当然有,和我们以前学习过多连接池技术类似,线程池通过提前创建好线程保存在线程池中, 在任务要执行时取

    2023年04月17日
    浏览(33)
  • SourceTree使用看这一篇就够了

     你梦想有一天成为git大师,然而面对复杂的git命令,你感觉TMD这我能记得住吗?你曾经羡慕从命令行敲git命令,才会更加炫酷,然而时间一长,TMD命令我有忘了。那么今天我介绍的这款工具会让你从git命令中解救出来,这就是git可视化工具SourcTree。 事实上Git的功能十分强大

    2024年02月08日
    浏览(26)
  • CAS自旋锁,看这一篇就够了

    前序 时隔多年,杰伦终于出了新专辑,《最伟大的作品》让我们穿越到1920年,见到了马格利特的绿苹果、大利的超现实、常玉画的大腿、莫奈的睡莲、徐志摩的诗… 他说“最伟大的作品”并不是自己的歌,而是这个世界上最伟大的艺术作品们。 为什么要写CAS自旋锁呢?最近

    2023年04月08日
    浏览(19)
  • ElasticSearch常见用法,看这一篇就够了

    2024送书福利正式起航 关注「哪吒编程」,提升Java技能 文末送3本《一本书讲透Elasticsearch:原理、进阶与工程实践》 大家好,我是哪吒。 ElasticSearch是一款由Java开发的开源搜索引擎,它以其出色的实时搜索、稳定可靠、快速安装和方便使用的特性,在Java开发社区中赢得了广

    2024年03月19日
    浏览(33)
  • 超图(HyperGraph)学习,看这一篇就够了

    最近事多,好久没更新了,随便写写(Ctrl+V)点 一、超图定义 通常图论中的图,一条edge只能连接2个vertex,在超图中,不限量 如何理解呢,就用我正在做的KT问题来看:7道题目-7个顶点;4种概念-4条超边,其中第1,2,3题都是考察概念1的,则构建一个包含了这仨的超边,以此类

    2024年02月02日
    浏览(32)
  • Docker Volume 看这一篇就够了

    默认情况下,在容器内创建的所有文件都存储在可写容器层上。这意味着: 当该容器不再存在时,数据不会持续存在,并且如果另一个进程需要数据,则可能很难将数据从容器中取出。 容器的可写层与运行容器的主机紧密耦合。您无法轻松地将数据移动到其他地方。 写入容

    2024年02月02日
    浏览(56)
  • 关于Tacotron2看这一篇就够了

    文章来源 [1712.05884] NATURAL TTS SYNTHESIS BY CONDITIONING WAVENET ON MEL SPECTROGRAM PREDICTIONS 参考博客 声谱预测网络(Tacotron2) Tacotron2 论文 + 代码详解 Tacotron2讲解 论文阅读 Tacotron2 Tacotron2 模型详解 Tacotron2-Details 简介: The system is composed of a recurrent sequence-to-sequence feature prediction network that m

    2024年02月09日
    浏览(18)
  • Java迭代器详解,看这一篇就够了

    迭代器 是属于 设计模式 之一, 迭代器模式 提供了一种方法来顺序访问一个聚合对象中各个元素,而不保留该对象的内部表示。 1) Iterator对象 称为 迭代器 ,主要用于遍历 Collection集合 中的元素。 2)所有实现了 Collection接口 的集合类都有一个 iterator() 方法,用以返回一个

    2024年02月02日
    浏览(23)
  • C++异常处理详解 看这一篇就够了

    在程序运行的过程中,我们不可能保证我们的程序百分百不出现异常和错误,那么出现异常时该怎么报错,让我们知道是哪个地方错误了呢? C++中就提供了异常处理的机制。 throw : 当问题出现时,程序会抛出一个异常。这是通过使用 throw 来完成的。 catch : 在您想要处理

    2024年02月14日
    浏览(17)
  • 背包问题(动态规划看这一篇就够了)

    题目描述     一个旅行者有一个最多能装M公斤的背包,现在有n件物品,他们的重量分别是W1,W2,......,Wn,他们的价值分别是C1,C2,......,Cn,求旅行者能获得的最大总价值。 输入     第一行:两个整数,M(M=200)和N(N=30);     第2至N+1行:每行两个整数Wi,Ci,表示每

    2024年04月15日
    浏览(18)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包