图的遍历
题目描述
给出\(N\)个点,\(M\)条边的有向图,对于每个点\(v\),求\(A(v)\)表示从点\(v\)出发,能到达的编号最大的点。
输入输出格式
输入格式
第1 行,2 个整数\(N,M\)。
接下来\(M\)行,每行2个整数\(U_i,V_i\),表示边\((U_i,V_i)\)。点用\(1, 2,\cdots,N\)编号。
输出格式
N 个整数\(A(1),A(2),\cdots,A(N)\)。
输入输出样例
输入样例 #1
4 3
1 2
2 4
4 3
输出样例 #1
4 4 3 4
说明
- 对于60% 的数据,\(1 \le N . M \le 10^3\);
- 对于100% 的数据,\(1 \le N , M \le 10^5\)。
分析
打眼一看,如果暴力算法,时间复杂度 \(\operatorname{O}(n^2)\)。
\(10^5\) 这个数据量,平方算法肯定劝退了,要不然 \(\operatorname{O}(n\log n)\) 要不然 \(\operatorname{O}(n)\)。\(\operatorname{O}(n\log n)\) 的时间复杂度算法我是没想出来,但是 \(\operatorname{O}(n)\) 的算法可以用逆向思维思考出。
什么是逆向思维呢?用本题举一个例子。本题让我们求每一个可以到达点编号最大的点,那么我们不仅要处理它可以到达的所有点,还要取最大值,而且一次处理只能处理一个,赔本买卖的结果就是TLE。
那么我们就可以逆向思维,思考编号大的点可以反向到达哪些点,这样,能到达的所有点如果之前没有被到达过,那么它的值肯定就是当前点编号了,一次设置,就再也不用变了。这样我们不仅可以一次处理就处理很多点,而且没有取最大值的操作,而且如果当前点之前就被到达,可以直接return,剪枝掉很多不必要的点。这样简直赚翻了。
具体实现过程是这样的:
- 反向建边
- 从N到1循环当前编号的点作为A点,并进行dfs
- 可以dfs到的点B点中,如果B点的A值之前没有被设置,那就设置成A点的编号。如果B点的A值之前设置了,那么就直接return。(因为如果A点到C点的一条路径经过了B点,那么B点与C点就是联通的,那么既然B点被处理过了,C点也一定被处理过)
- 处理完后,输出每个点的A值,结束程序
上代码:
代码
/*
* @Author: crab-in-the-northeast
* @Date: 2020-09-16 12:13:33
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2020-09-19 10:46:44
*/
#include <iostream>
#include <cstdio>
#include <vector>
const int maxn = 100005;
int A[maxn];
std :: vector <int> rev_side[maxn];
void dfs(int u, int num) {
A[u] = num;
for (int i = 0; i < rev_side[u].size(); ++i) {
int v = rev_side[u][i];
if (!A[v])
dfs(v, num);
}
}
int main() {
int n, m;
std :: scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
std :: scanf("%d %d", &u, &v);
rev_side[v].push_back(u);
}
for (int i = n; i; --i) {
if (!A[i])
dfs(i, i);
}
for (int i = 1; i <= n; ++i)
std :: printf("%d ", A[i]);
putc('\n', stdout);
return 0;
}