图的遍历
题目描述
给出(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}(nlog n)) 要不然 (operatorname{O}(n))。(operatorname{O}(nlog 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('
', stdout);
return 0;
}