反向建图+拓扑排序
反向建图+拓扑排序
零、复习拓扑排序
【正图,普通拓扑排序】
题意:给出人的编号为 到,再给出个关系。和,是的老师。问这些关系是否存在矛盾,即不能存在是的老师,是的老师,而是的老师。
思路:很容易发现,存在矛盾的样例的图一定存在环。而 拓扑排序是判断是否有环的很好算法。即如果从队列中取出的点不等于,就一定存在环。
注:不能由队列是否为空来判断,当时队列也为空,当这是有环的。只有当每个点取出,才可以确定没有环。
#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;
}
题意:老板发奖金,每个工人至少元,而有些工人存在着要求,和,要求他的奖金要比高。给你个工人,编号为到,个要求,求老板总共最低需要给这些工人多少奖金。不能满足他们的要求就输出。
思路:不能满足要求,即存在环。当满足要求时,最小的得,向上依次加。
#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;
}
一、基本认识
- 当有向图需要求多个点到一个点的最短距离时
- 除了使用多元最短路的方法还可以通过反向建边转化为单源最短路
- 顾名思义,将边反向
二、反向建边方法
- 用邻接表存图时(用堆优化版跑最短路时适用
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、多点到一点的最短距离,需要反向建图
题目描述
有一个邮递员要送东西,邮局在节点 。他总共要送 样东西,其目的地分别是节点 到节点 。由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 条道路。这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。求送完这 样东西并且最终回到邮局最少需要的时间。
输入格式
第一行包括两个整数, 和 ,表示城市的节点数量和道路数量。
第二行到第 行,每行三个整数,,表示从 到 有一条通过时间为 的道路。
输出格式
输出仅一行,包含一个整数,为最少需要的时间。
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;
}
图的遍历
题目描述
给出 个点, 条边的有向图,对于每个点 ,求 表示从点 出发,能到达的编号最大的点。
输入格式
第 行 个整数 ,表示点数和边数。
接下来 行,每行 个整数 ,表示边 。点用 编号。
输出格式
一行 个整数 。
样例输入
4 3
1 2
2 4
4 3
样例输出
4 4 3 4
提示
- 对于 的数据,。
- 对于 的数据,。
解题思路
一开始大家的思路应该就是单纯的但是我们注意到的范围到了 ,这时 的暴力是过不去的,于是我换了个思路,按题目来每次考虑每个点可以到达点编号最大的点,不如考虑较大的点可以反向到达哪些点。
循环从到,则每个点能访问到的结点的值都是。
每个点访问一次,这个值就是最优的,因为之后如果再访问到这个结点那么答案肯定没当前大了,这样做的同时也避免了每次都要更新状态值。
#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、对于要求编号小的靠前输出这类题需要反向建图
:为什么这样的要求需要反向建图 ?
比如说:有 共个数,要求 在 前面输出(对 没有要求)
如果没有反向建图的话,会造成输出 这样的错解(实际上应该输出 ...)
详细步骤
对于给定顺序:
(对于这一行,要求先输出,再输出,最后输出 )
(同上)
(同上)
这样我们反向建图,每次都处理(比较)入度为 的点
这样我们先比较 这一列( 入度为 嘛),大的肯定是最后输出(要求小的先输出)
因此 ,把 剔除出去,与 相连的点的入度
此时入度为 的点有 ,然后再把最大的剔除出去
因此 ,与 相连的点的入度
此时入度为 的点有 ,然后再把最大的剔除出去
因此 与 相连的点的入度 。
反复执行上述步骤,直至处理完全部入度为 的点( 可能有环。。。)
最后得到
此时,我们再反向输出,即可以得到答案
求拓扑有两种形式
:这两种题目有什么不同呢?
我们发现前一道是要求 字典序最小优先输出 即可 后面一道却是要求 编号最小优先输出!
对于这张图,字典序最小优先输出 是 1 3 4 2
, 编号最小优先输出 是 1 4 2 3
思路:
首先这道题 给定了两个数之间的位次关系 ,那么显然是需要用到 拓扑排序 的.
输出不可行的方法 也十分简单,只要 使用拓扑排序判断一下有没有形成环 即可,因为假设形成了环了话,那么我们的这个环就不存在一个点的入度为,那么这个点就不会加入到中,最终的答案队列中的数就不够个了,所以此时直接输出impossible!
即可。
那么接下来的主要解决问题就是 如何保证越小的值越在前面 这个问题了.首先我们肯定会想到使用贪心输出字典序最小的序列,但是遗憾的是,这个贪心显然是错误的.举个反例:
种菜肴,限制为 那么字典序最小的是 但题目要求的最优解是 这种菜肴,所以我们直接使用贪心做是不行的.
贪心:在前,在前,而无顺序关联,无顺序关联,按字典序的话,就是
题目要求:先考虑,需要在后,即,然后考虑,需要在前,就是
总结:字典序最小与编号最小是两回事,用不同的方法实现!
思路:在当前步骤,在所有入度为的点中 输出编号最小的。考虑用实现,修改的拓扑排序程序,把普通队列改为优先队列。在中放入度为的点,每次输出编号最小的结点,然后把它的后续结点的入度减一,入度减为的再放进,这样就能输出一个字典序最小的拓扑排序。
#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;
}
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2013-11-13 广州项目实施步骤III_练习使用Keepalive保证HaProxy的高可用性
2013-11-13 广州项目实施步骤II_练习配置HaProxy的重定向负载均衡