【题目来源】
https://www.luogu.com.cn/problem/P1726
【题目描述】
在幻想乡,上白泽慧音是以知识渊博闻名的老师。春雪异变导致人间之里的很多道路都被大雪堵塞,使有的学生不能顺利地到达慧音所在的村庄。因此慧音决定换一个能够聚集最多人数的村庄作为新的教学地点。
人间之里由 N 个村庄(编号为 1⋯N)和 M 条道路组成,道路分为两种一种为单向通行的,一种为双向通行的,分别用 1 和 2 来标记。如果存在由村庄 A 到达村庄 B 的通路,那么我们认为可以从村庄 A 到达村庄 B,记为 (A,B)。当 (A,B) 和 (B,A) 同时满足时,我们认为 A,B 是绝对连通的,记为 〈A,B〉。绝对连通区域是指一个村庄的集合,在这个集合中任意两个村庄 X,Y 都满足 〈X,Y〉。现在你的任务是,找出最大的绝对连通区域,并将这个绝对连通区域的村庄按编号依次输出。若存在两个最大的,输出字典序最小的,比如当存在 1,3,4 和 2,5,6 这两个最大连通区域时,输出的是 1,3,4。
【输入格式】
第一行共两个正整数 N,M。
第 2 行至第 M+1 行,每行有三个正整数 a,b,t。若 t=1 则表示存在从村庄 a 到 b 的单向道路,若 t=2 表示村庄 a,b 之间存在双向通行的道路。保证每条道路只出现一次。
【输出格式】
第一行输出 1 个整数,表示最大的绝对连通区域包含的村庄个数。
第二行输出若干个整数,依次输出最大的绝对连通区域所包含的村庄编号。
【输入样例】
5 5
1 2 1
1 3 2
2 4 2
5 1 2
3 5 1
【输出样例】
3
1 3 5
【说明/提示】
对于 60% 的数据,1≤N≤200,且 0≤M≤10^4;
对于 100% 的数据,1≤N≤5×10^3,且 0≤M≤5×10^4。
【算法分析】
● Tarjan算法简介
Tarjan算法是一个基于深搜(DFS),用于求解图中强连通分量(SCC)和割点/割边问题的算法。
Tarjan算法在搜索的过程中,会形成一棵搜索树。
由上图易知,横向边和前向边都无法构成回路,即不能形成大于一个点的强连通分量。所以,在本题中应用 Tarjan 算法的关键,就是找出重要的后向边,用于求解图中的强连通分量。
● Tarjan算法中两个核心数组 dfn[x] 与 low[x]
dfn[x]:表示一个结点的时间戳。时间戳是在 dfs 的过程中,每个结点第一次被访问的时间顺序。
low[x]:表示一个结点的追溯值。追溯值是一个结点通过后向边能够回到的最早结点的编号,也就是 dfn 的最小值。
dfn 可以通过两种方式更新:
(1)若在搜索树上 x 是 y 的父结点,那么 low[x]=min(low[x],low[y])
(2)若是非树边(横向边、后向边),那么 low[x]=min(low[x],dfn[y])
要注意的是,在无向图中,儿子到父亲的那条边不处理,要不然 low 值就都是 1 了,没有意义。
● Tarjan算法关于图中强连通分量的判定条件
与当前点 cur 关联的所有边都被遍历完之后,判断它的 dfn 是否等于 low(即,dfn[cur] == low[cur])?若为 True,则表示搜到了一个强连通分量的根。之后,出栈,直到当前结点 cur 终止。出栈的都是这个强连通分量集合内的结点;若为 False,回溯。
● 本题可考虑使用优先队列实现按字典序输出结果。
优先队列:https://blog.csdn.net/qq_19656301/article/details/82490601
#include <bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,less<int> > down;
priority_queue<int,vector<int>,greater<int> > up;
int main() {
int n,x;
cin>>n;
while(n--) {
cin>>x;
down.push(x);
up.push(x);
}
while(!down.empty()) {
cout<<down.top()<<" ";
down.pop();
}
cout<<endl;
while(!up.empty()) {
cout<<up.top()<<" ";
up.pop();
}
return 0;
}
/*
in:
5
6 9 2 7 1
out:
9 7 6 2 1
1 2 6 7 9
*/
● 链式前向星:https://blog.csdn.net/hnjzsyjyj/article/details/139369904
val[idx]:存储编号为 idx 的边的值
e[idx]:存储编号为 idx 的结点的值
ne[idx]:存储编号为 idx 的结点指向的结点的编号
h[a]:存储头结点 a 指向的结点的编号
【算法代码一】
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e3+5;
vector<int> g[maxn];
stack<int> stk;
int scc[maxn]; //scc[i]:第i个强连通分量的结点数
int cnt_scc;
int dfn[maxn]; //dfn[i]:结点i的时间戳
int low[maxn]; //low[i]:结点i的追溯值。也就是dfn的最小值。
int vis[maxn];
int cnt[maxn];
int ts; //时间戳 timestamp
void tarjan(int u) {
dfn[u]=low[u]=++ts;
stk.push(u);
vis[u]=1;
for(int t:g[u]) {
if(dfn[t]==0) {
tarjan(t);
low[u]=min(low[u],low[t]);
} else if(vis[t]) {
low[u]=min(low[u],low[t]);
}
}
if(low[u]==dfn[u]) {
cnt_scc++;
while(stk.top()!=u) {
int t=stk.top();
vis[t]=0;
scc[t]=cnt_scc;
cnt[cnt_scc]++;
stk.pop();
}
scc[u]=cnt_scc;
stk.pop();
vis[u]=0;
cnt[cnt_scc]++;
}
}
int main() {
int n,m;
cin>>n>>m;
for(int i=1; i<=m; i++) {
int x,y,op;
cin>>x>>y>>op;
g[x].push_back(y);
if(op==2) g[y].push_back(x);
}
for(int i=1; i<=n; i++) {
if(!dfn[i]) tarjan(i);
}
int t=0, ans=-1;
for(int i=1; i<=n; i++) {
if(ans<cnt[scc[i]]) {
ans=cnt[scc[i]];
t=scc[i];
}
}
cout<<ans<<endl;
int cur=0;
for(int i=1; i<=n; i++) {
if(scc[i]==t) {
if(cur++) cout<<" ";
cout<<i;
}
}
cout<<endl;
return 0;
}
/*
in:
5 5
1 2 1
1 3 2
2 4 2
5 1 2
3 5 1
out:
3
1 3 5
*/
【算法代码二】
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
const int inf=0x3f3f3f3f;
int dfn[maxn]; //dfn[i]:结点i的时间戳
int low[maxn]; //low[i]:结点i所能到达的最小时间戳
int lie[maxn]; //lie[i]:结点i位于的强联通分量
int siz[maxn]; //siz[i]:第i个强联通分量的结点数
int stk[maxn]; //模拟栈
int top; //栈顶
int tot; //第tot个强联通分量
int ts; //时间戳 timestamp
int ans=-inf;
int n,m;
int e[maxn<<1],ne[maxn<<1],h[maxn],idx;
void add(int a,int b) {
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u) {
dfn[u]=low[u]=++ts;
stk[++top]=u;
for(int i=h[u]; ~i; i=ne[i]) {
int v=e[i];
if(!dfn[v]) {
tarjan(v);
low[u]=min(low[u],low[v]);
} else if(!lie[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]) {
lie[u]=++tot;
siz[tot]++;
while(stk[top]!=u) {
siz[tot]++;
lie[stk[top]]=tot;
top--;
}
top--;
}
}
int main() {
memset(h,-1,sizeof(h));
cin>>n>>m;
for(int i=1; i<=m; i++) {
int x,y,f;
cin>>x>>y>>f;
if(f==1) add(x,y);
if(f==2) add(x,y),add(y,x);
}
for(int i=1; i<=n; i++) {
if(!dfn[i]) tarjan(i);
}
for(int i=1; i<=tot; i++) ans=max(ans,siz[i]);
cout<<ans<<endl;
for(int i=1; i<=n; i++) {
if(siz[lie[i]]==ans) {
int cur=lie[i];
for(int j=i; j<=n; j++) {
if(lie[j]==cur) cout<<j<<" ";
}
return 0;
}
}
return 0;
}
/*
in:
5 5
1 2 1
1 3 2
2 4 2
5 1 2
3 5 1
out:
3
1 3 5
*/
【参考文献】
https://www.cnblogs.com/dgsvygd/p/16579748.html
https://www.cnblogs.com/bloodstalk/p/17432793.html
https://blog.csdn.net/yikeyoucaihua/article/details/135832822
https://www.luogu.com.cn/problem/solution/P1726
https://www.luogu.com.cn/article/6dgk1yk1
https://blog.csdn.net/qq_30277239/article/details/118683637
https://zhuanlan.zhihu.com/p/161537289