惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Scott Helme
Scott Helme
N
Netflix TechBlog - Medium
AI
AI
Security Latest
Security Latest
GbyAI
GbyAI
P
Proofpoint News Feed
Y
Y Combinator Blog
A
Arctic Wolf
G
Google Developers Blog
U
Unit 42
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
T
Tor Project blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
L
Lohrmann on Cybersecurity
C
CERT Recently Published Vulnerability Notes
C
Check Point Blog
B
Blog RSS Feed
The GitHub Blog
The GitHub Blog
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 【当耐特】
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Microsoft Security Blog
Microsoft Security Blog
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
Latest news
Latest news
L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
WordPress大学
WordPress大学
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
The Hacker News
The Hacker News
Simon Willison's Weblog
Simon Willison's Weblog
V
V2EX
Project Zero
Project Zero
博客园_首页

博客园 - 糖豆爸爸

【树上DP前导知识汇总】 快速幂、龟速乘总结 卡特兰数专题(Catalan) AcWing 126. 最大的和 AcWing 431. 守望者的逃离 AcWing 414. 数字游戏 AcWing 468. 魔法阵 AcWing 463. 求和 洛谷 P1632 点的移动 P1056 NOIP2008 普及组 排座椅 洛谷 P1889 士兵站队 洛谷 P1862 输油管道问题 CF444C DZY Loves Colors P2253 好一个一中腰鼓! SCOI2010 P2572 序列操作 P4344 SHOI2015 脑洞治疗仪 T125847 【模板】动态开点线段树 P3373 【模板】线段树 2 Physical Education Lessons
反向建图+拓扑排序
糖豆爸爸 · 2023-11-13 · via 博客园 - 糖豆爸爸

反向建图+拓扑排序

零、复习拓扑排序

\(HDU\) \(3342\) \(Legal\) \(or\) \(Not\)

【正图,普通拓扑排序】

题意:给出\(n\)人的编号为 \(0\)\(n-1\),再给出\(m\)个关系。\(A\)\(B\)\(A\)\(B\)的老师。问这些关系是否存在矛盾,即不能存在\(A\)\(B\)的老师,\(B\)\(C\)的老师,而\(C\)\(A\)的老师。

思路:很容易发现,存在矛盾的样例的图一定存在环。而 拓扑排序是判断是否有环的很好算法。即如果从队列中取出的点不等于\(n\),就一定存在环。

:不能由队列是否为空来判断,当\(n - 2, 1->2, 2->1\)时队列也为空,当这是有环的。只有当每个点取出,才可以确定没有环。

#include <bits/stdc++.h>
using namespace std;

const int N = 110, M = N << 1;
int in[N]; // 入度
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

bool solve(int n) {
    queue<int> q;
    for (int i = 0; i < n; i++)
        if (in[i] == 0) q.push(i);

    int cnt = 0;
    while (q.size()) {
        int u = q.front();
        q.pop();

        ++cnt;
        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            in[j]--;
            if (in[j] == 0) q.push(j);
        }
    }
    return cnt == n;
}

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    int n, m;
    while (cin >> n >> m && n) {
        // 多组测试数据,一定要注意两个初始语句!
        memset(h, -1, sizeof h);
        idx = 0;
        memset(in, 0, sizeof in);

        while (m--) {
            int u, v;
            cin >> u >> v;
            in[v]++;
            add(u, v); // 建正图
        }
        bool ans = solve(n);
        puts(ans ? "YES" : "NO");
    }
    return 0;
}

\(HDU\) \(2647\) \(Reward\) 奖励

题意:老板发奖金,每个工人至少\(888\)元,而有些工人存在着要求,\(a\)\(b\)\(a\)要求他的奖金要比\(b\)高。给你\(n\)个工人,编号为\(1\)\(n\)\(m\)个要求,求老板总共最低需要给这些工人多少奖金。不能满足他们的要求就输出\(-1\)

思路:不能满足要求,即存在环。当满足要求时,最小的\(b\)\(888\),向上依次加\(1\)

#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = N << 1;

// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int num[N]; // 记录每个工人的最终获取多少奖金
int n, m;
int in[N], ans;

int toposort() {
    queue<int> q;
    int cnt = 0; // 出队列的节点个数
    ans = 0;
    for (int i = 1; i <= n; i++)
        if (in[i] == 0) q.push(i);

    while (q.size()) {
        int u = q.front();
        q.pop();
        cnt++;
        ans += num[u]; // 出队时,累加出队人员的奖金数量

        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            in[j]--;
            if (in[j] == 0) {
                q.push(j);
                num[j] = num[u] + 1; // 需要表示反向图指定的节点比源节点的奖金大1
            }
        }
    }
    if (cnt != n) ans = -1;
    return ans;
}

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    while (cin >> n >> m) {
        // 初始化链式前向星
        memset(h, -1, sizeof h);
        idx = 0;
        memset(in, 0, sizeof in);
        for (int i = 0; i <= n; i++) num[i] = 888; // 每个人最少888元

        while (m--) {
            int u, v;
            cin >> u >> v;
            add(v, u); // 小的向大的建图
            in[u]++;
        }
        printf("%d\n", toposort());
    }
    return 0;
}

一、基本认识

  • 当有向图需要求多个点到一个点的最短距离时
  • 除了使用多元最短路的方法还可以通过反向建边转化为单源最短路
  • 顾名思义,将边反向

二、反向建边方法

  • 用邻接表存图时(用堆优化版\(dijkstra\)跑最短路时适用
int dist[2 * N];

// 链式前向星,因为是两份图,所以啥啥都是两份
int h[2 * N], ne[2 * M], e[2 * M], w[2 * M], idx;
bool st[N];
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

while (m--) {
    int a, b, c;
    cin >> a >> b >> c;
    add(a, b, c);         // 正图
    add(b + n, a + n, c); // 反图
}
  • 翻转邻接矩阵
void over(int n){//翻转
    for(i=1;i<=n;i++)
        for(j=i+1;j<=n;j++)
            swap(g[i][j], g[j][i]);
}

三、什么类型的题目需要反向建图 ?

1、多点到一点的最短距离,需要反向建图

邮递员送信

题目描述
有一个邮递员要送东西,邮局在节点 \(1\)。他总共要送 \(n-1\) 样东西,其目的地分别是节点 \(2\) 到节点 \(n\)。由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 \(m\) 条道路。这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。求送完这 \(n-1\) 样东西并且最终回到邮局最少需要的时间。

输入格式
第一行包括两个整数,\(n\)\(m\),表示城市的节点数量和道路数量。

第二行到第 \((m+1)\) 行,每行三个整数,\(u,v,w\),表示从 \(u\)\(v\) 有一条通过时间为 \(w\) 的道路。

输出格式
输出仅一行,包含一个整数,为最少需要的时间。

input    output  83
2 3 5
1 5 5
3 5 6
1 2 8
1 3 8
5 3 4
4 1 8
4 5 3
3 5 6
5 4 2
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10, M = N << 1;
typedef pair<int, int> PII;

int n, m, s;
int dist[2 * N];

// 链式前向星,因为是两份图,所以啥啥都是两份
int h[2 * N], ne[2 * M], e[2 * M], w[2 * M], idx;
bool st[N];
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void dijkstra(int s) {
    memset(dist, 0x3f, sizeof dist);
    dist[s] = 0;

    priority_queue<PII, vector<PII>, greater<PII>> q; // 小顶堆
    q.push({0, s});

    while (q.size()) {
        auto t = q.top();
        q.pop();

        int u = t.second;
        if (st[u]) continue;
        st[u] = 1;

        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[u] + w[i]) {
                dist[j] = dist[u] + w[i];
                q.push({dist[j], j});
            }
        }
    }
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m--) {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);         // 正图
        add(b + n, a + n, c); // 反图
    }

    int res = 0;
    // 单源最短路,就是从1出发,到达其它n-1个点的最短距离
    dijkstra(1);
    for (int i = 1; i <= n; i++) res += dist[i];

    // 从n-1个节点,返回1号节点,就是多源最短路
    // 通过建反图,将多源最短路转化为单源最短路
    // 建反图时,所以节点号+n处理,这样1号节点就变成1+n号节点,其它n-1个节点向1号节点的多源最短路就变成了
    // 从1+n~ [2+n,3+n,...,n+n]的最短路径了
    dijkstra(1 + n);
    for (int i = 1 + n; i <= 2 * n; i++) res += dist[i];

    printf("%d\n", res);
    return 0;
}

\(P3916\) 图的遍历
题目描述

给出 \(N\) 个点,\(M\) 条边的有向图,对于每个点 \(v\),求 \(A(v)\) 表示从点 \(v\) 出发,能到达的编号最大的点。

输入格式

\(1\)\(2\) 个整数 \(N,M\),表示点数和边数。

接下来 \(M\) 行,每行 \(2\) 个整数 \(U_i,V_i\),表示边 \((U_i,V_i)\)。点用 \(1,2,\dots,N\) 编号。

输出格式

一行 \(N\) 个整数 \(A(1),A(2),\dots,A(N)\)

样例输入

4 3
1 2
2 4
4 3

样例输出

4 4 3 4

提示

  • 对于 \(60\%\) 的数据,\(1 \leq N,M \leq 10^3\)
  • 对于 \(100\%\) 的数据,\(1 \leq N,M \leq 10^5\)

解题思路
一开始大家的思路应该就是单纯的\(dfs\)但是我们注意到\(n,m\)的范围到了\(10^5\) ,这时\(O(N^2)\) 的暴力是过不去的,于是我换了个思路,按题目来每次考虑每个点可以到达点编号最大的点,不如考虑较大的点可以反向到达哪些点。

循环从\(N\)\(1\),则每个点\(i\)能访问到的结点的\(A\)值都是\(i\)

每个点访问一次,这个\(A\)值就是最优的,因为之后如果再访问到这个结点那么答案肯定没当前大了,这样做的同时也避免了每次都要更新状态值。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N << 1;

int n, m;
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int res[N];

void dfs(int u, int val) {
    if (res[u]) return; // 如果已经标记过,不用重复标记
    res[u] = val;       // 没有标记过,就标记一下

    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!res[j]) dfs(j, val);
    }
}
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m--) {
        int a, b;
        cin >> a >> b;
        add(b, a);
    }

    for (int i = n; i >= 1; i--) dfs(i, i);
    for (int i = 1; i <= n; i++) cout << res[i] << ' ';
    return 0;
}

2、对于要求编号小的靠前输出这类题需要反向建图

\(Q\):为什么这样的要求需要反向建图 ?
比如说:有\(1,2,3\)\(3\)个数,要求 \(3\)\(1\) 前面输出(对 \(2\) 没有要求)

如果没有反向建图的话,会造成输出 \(2, 3, 1\)这样的错解(实际上应该输出 \(3,1,2\) ...)

详细步骤
对于给定顺序:
\(6 -> 4 -> 7\) (对于这一行,要求先输出\(6\),再输出\(4\),最后输出 \(7\))
\(3 -> 9 ->2\) (同上)
\(5 ->1 ->8\) (同上)

这样我们反向建图,每次都处理(比较)入度为 \(0\) 的点
\(6 <- 4 <- 7\)
\(3 <- 9 <- 2\)
\(5 <- 1 <- 8\)

这样我们先比较 \(7,2,8\) 这一列( 入度为 \(0\) 嘛),大的肯定是最后输出(要求小的先输出)

因此 \(ans[1] = 8\) ,把 \(8\) 剔除出去,与 \(8\) 相连的点的入度 \(-1\)
\(6 <- 4 <- 7\)
\(3 <- 9 <- 2\)
\(5 <- 1\)
此时入度为 \(0\) 的点有 \(7,2,1\) ,然后再把最大的剔除出去

因此 \(ans[2] = 7\),与 \(7\) 相连的点的入度 \(-1\)
\(6 <- 4\)
\(3 <- 9 <- 2\)
\(5 <- 1\)

此时入度为 \(0\) 的点有 \(4,2,1\) ,然后再把最大的剔除出去

因此 \(ans[3] = 4\)\(4\) 相连的点的入度 \(-1\)

反复执行上述步骤,直至处理完全部入度为 \(0\) 的点( 可能有环。。。)

最后得到 \(ans[1] = 8 ; ans[2] = 7 ;ans[3] = 4 ; ans[4] = 6 ;ans[5] = 2 ; ans[6] = 9 ;ans[7] = 3 ;ans[8] = 1 ; ans[9] = 5\)

此时,我们再反向输出,即可以得到答案
\(5, 1, 3, 9, 2, 6, 4, 7, 8\)

求拓扑有两种形式

\(Q\):这两种题目有什么不同呢?

我们发现前一道是要求 字典序最小优先输出 即可 后面一道却是要求 编号最小优先输出

对于这张图,字典序最小优先输出1 3 4 2

\(BUT\), 编号最小优先输出1 4 2 3

思路:

首先这道题 给定了两个数之间的位次关系 ,那么显然是需要用到 拓扑排序 的.

输出不可行的方法 也十分简单,只要 使用拓扑排序判断一下有没有形成环 即可,因为假设形成了环了话,那么我们的这个环就不存在一个点的入度为\(0\),那么这个点就不会加入到\(bfs\)中,最终的答案队列中的数就不够\(n\)个了,所以此时直接输出impossible!即可。

那么接下来的主要解决问题就是 如何保证越小的值越在前面 这个问题了.首先我们肯定会想到使用贪心输出字典序最小的序列,但是遗憾的是,这个贪心显然是错误的.举个反例:

\(4\)种菜肴,限制为 \(<2,4> <3,1>\) 那么字典序最小的是 \(2,3,1,4\) 但题目要求的最优解是 \(3,1,2,4\)\(4\)种菜肴,所以我们直接使用贪心做是不行的.

贪心\(2\)\(4\)前,\(3\)\(1\)前,而\(2、3\)无顺序关联,\(4、1\)无顺序关联,按字典序的话,就是\(2、3、1、4\)
题目要求:先考虑\(1\),需要在\(3\)后,即\(3、1\),然后考虑\(2\),需要在\(4\)前,就是\(3、1、2、4\)
总结:字典序最小与编号最小是两回事,用不同的方法实现!

\(HDU\) \(1285\) 确定比赛名次

思路:在当前步骤,在所有入度为\(0\)的点中 输出编号最小的。考虑用\(BFS\)实现,修改\(BFS\)的拓扑排序程序,把普通队列改为优先队列\(Q\)。在\(Q\)中放入度为\(0\)的点,每次输出编号最小的结点,然后把它的后续结点的入度减一,入度减为\(0\)的再放进\(Q\),这样就能输出一个字典序最小的拓扑排序。

#include <bits/stdc++.h>
using namespace std;
const int N = 510;

bool g[N][N]; // 邻接矩阵
int in[N];    // 入度
int n, m;     // n个顶点,m条边

// 小顶堆
priority_queue<int, vector<int>, greater<int>> q;

/*
4 3
1 2
2 3
4 3

1 2 4 3
*/
void toposort() {
    // 所有入度为零的节点入队列,是拓扑序的起始节点
    for (int i = 1; i <= n; i++)
        if (in[i] == 0) q.push(i);

    int cnt = 0;
    while (q.size()) {
        int u = q.top();
        q.pop();
        cnt++; // 又一个节点出队列

        // 输出拓扑序
        if (cnt != n)
            cout << u << " ";
        else
            cout << u << endl;

        for (int i = 1; i <= n; i++)
            if (g[u][i]) {
                in[i]--;
                if (!in[i]) q.push(i);
            }
    }
}

int main() {
    while (cin >> n >> m) {
        memset(g, 0, sizeof g);
        memset(in, 0, sizeof in);

        while (m--) {
            int x, y;
            cin >> x >> y;
            if (g[x][y]) continue; // 防止重边
            g[x][y] = 1;           // 设置有关联关系
            in[y]++;               // 入度+1
        }
        toposort(); // 拓扑序
    }
    return 0;
}

牛客网 菜肴制作

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100010, M = N << 1;
int in[N]; // 入度数组

// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void solve() {
    memset(in, 0, sizeof in); // 入度
    memset(h, -1, sizeof h);  // 初始化链式前向星
    idx = 0;

    priority_queue<int> q;         // 优先队列大顶堆
    map<pair<int, int>, int> _map; // 使用红黑树保存已经添加过关系的数,防止出现重边
    vector<int> ans;
    int n, m;
    cin >> n >> m; // n个菜,m个关系
    while (m--) {
        int x, y;
        cin >> x >> y;
        if (!_map[{x, y}]) { // 防止重复建边
            _map[{x, y}] = 1;
            add(y, x); // 反向建图
            in[x]++;   // 记录入度
        }
    }
    // 入度为零的入队列
    for (int i = 1; i <= n; i++)
        if (!in[i]) q.push(i);

    // Topsort
    while (q.size()) {
        int u = q.top(); // 序号大的先出来,然后倒序输出
        q.pop();
        ans.push_back(u); // 记录出队顺序
        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            in[j]--;
            if (!in[j]) q.push(j);
        }
    }
    if (ans.size() != n)
        cout << "Impossible!";
    else
        for (int i = n - 1; i >= 0; i--) cout << ans[i] << ' ';
    cout << endl;
}

signed main() {
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}

\(HDU\) \(4857\) 逃生【拓扑排序反向建图】

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, M = N << 1;

int n, m, ans[N], al;
int in[N];

// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void topsort() {
    priority_queue<int> q;
    for (int i = 1; i <= n; i++)
        if (in[i] == 0) q.push(i);

    while (q.size()) {
        int u = q.top();
        q.pop();
        ans[al++] = u;
        for (int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            in[j]--;
            if (in[j] == 0) q.push(j);
        }
    }
}
void solve() {
    cin >> n >> m;
    al = 0, idx = 0;
    memset(h, -1, sizeof h);

    while (m--) {
        int u, v;
        cin >> u >> v;
        add(v, u);
        in[u]++;
    }
    topsort();
    for (int i = al - 1; i >= 0; i--) {
        cout << ans[i];
        if (i != 0)
            cout << ' ';
        else
            cout << endl;
    }
}
int main() {
    ios::sync_with_stdio(false), cin.tie(0);
    int T;
    cin >> T;
    while (T--) solve();
    return 0;
}