题解:P3388 【模板】割点(割顶)
huhaoxuan123456 · · 题解
简介:
一种由Robert Tarjan提出的求解有向图强连通分量的线性时间的算法。
代码:
割点(割顶):
理解1:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
#define x first
#define y second
const int mod=1e9+7;
const int N=2e4+50;
const int M=1e5+50;
#define endl '\n'
//#define int ll
#define IO ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int head[N];
struct edge{
int u,v,nxt;
}e[M<<1];
int n,m;
int tot=0;
bool is_cut[N]={};
void add_edge(int u,int v){ //链式前向星建边
e[++tot]=edge{u,v,head[u]};
head[u]=tot;
}
int id=0; //表示时间戳
//low 表示在 u 的子树中能够回溯到的最早的已经在栈中的结点,更简单点讲就是表示这个点双路(无重复点)的最小时间戳
int dfn[N]={},low[N]={}; //dfn 表示这个节点的时间戳
void tarjan(int u,int root){ //Tarjan 算法函数
int cnt=0; //cnt 表示 u 这个点的子节点
dfn[u]=low[u]=++id; //表示初始化,都初始化为时间戳
for(int i=head[u];i;i=e[i].nxt){ //遍历每一条边
int v=e[i].v; //表示每一条边的终点
if(!dfn[v]){ //如果是一个新的节点
cnt++; //+1
tarjan(v,root); //递归,进行初始化
low[u]=min(low[u],low[v]); //比较最小时间戳
if(u!=root&&dfn[u]<=low[v]){
//如果说 u 节点不是 root 节点,并且 dfn[u]<=low[v],就说明 v 节点和它的祖先之间的路线必有 u 节点,也就进一步说明 u 节点是一个割点
is_cut[u]=1; //标记为割点
}
}else{
low[u]=min(low[u],dfn[v]); //否则,就说明 v 节点是 u 节点的祖先,而且因为 v 节点已被访问,所以只能跟 dfn[v] 比最小时间戳
}
}
if(u==root&&cnt>1){ //判断,如果 u 节点是根节点,且有大于 1 个的子节点的话,就能成为割点
is_cut[u]=1; //标记
}
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// IO;
cin>>n>>m; //输入
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
add_edge(x,y); //连边
add_edge(y,x);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
//判断,如果这个节点没有在上个联通块遍历的时候添加时间戳,就说明这个节点所处的是一个新的联通块,因为是首次发现这个新的联通块,所以就从这个点开始,遍历这个新的联通块,并且把这个点作为这个新联通块的根节点
tarjan(i,i); //遍历这个新的联通块
}
}
int ans=0; //表示割点的个数
for(int i=1;i<=n;i++){
if(is_cut[i]==1){ //判断
ans++; //+1
}
}
cout<<ans<<endl; //输出
for(int i=1;i<=n;i++){
if(is_cut[i]==1){ //判断
cout<<i<<" "; //输出
}
}
return 0;
}
//参考题目:https://www.luogu.com.cn/problem/P3388
理解2:
其实,我们还可以用另外一种理解方式:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
#define x first
#define y second
const int mod=1e9+7;
const int N=1e5+50;
#define endl '\n'
//#define int ll
#define IO ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n,m;
struct edge{ //链式前向星建边
int u,v,nxt;
}e[N<<1];
int head[20005]={};
int tot=0;
void addedge(int u,int v){ //插入一条边
e[++tot]=edge{u,v,head[u]};
head[u]=tot;
}
int dfn[20005]={},low[20005]={};
//dfn[i] 是按照 dfs 序的到第 i 个点的时间戳,low[i] 表示第 i 个点在双路(无重复点)的前提下能到达的最小时间戳
int is_cut[20005]={}; //is_cut[i] 表示第 i 个点是否是割点
int id=0;
void tarjan(int u,int root){ //Tarjan 算法函数
int cnt=0; //表示我 u 节点有多少个孩子
dfn[u]=low[u]=++id; //初始化,low[u] 最坏情况下也是自己本身的时间戳
for(int i=head[u];i;i=e[i].nxt){ //遍历边
int v=e[i].v;
if(!dfn[v]){ //如果 v 是 u 的孩子的话
cnt++; //+1
tarjan(v,root); //递归
low[u]=min(low[u],low[v]); //原理请看下方,①
if(u!=root&&low[v]>=dfn[u]){ //如果说low[v]>=dfn[u] 就是说 v 无法在双路(无重复点)的前提下能到达 u 的上面,说明 v 到 u 的上面必须要经过 u 点,那么也就说明 u 点就是割点
is_cut[u]=1; //标记
}
}else{
low[u]=min(low[u],dfn[v]);//原理请看下方,②
}
}
if(u==root&&cnt>1){ //原因请看下方,③
is_cut[u]=1; //标记
}
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// IO;
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
addedge(u,v);
addedge(v,u);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
//判断,如果这个节点没有在上个联通块遍历的时候添加时间戳,就说明这个节点所处的是一个新的联通块,因为是首次发现这个新的联通块,所以就从这个点开始,遍历这个新的联通块,并且把这个点作为这个新联通块的根节点
tarjan(i,i); //遍历这个新的联通块
}
}
int ans=0;
for(int i=1;i<=n;i++){
if(is_cut[i]==1){
ans++; //+1
}
}
cout<<ans<<endl;
for(int i=1;i<=n;i++){
if(is_cut[i]==1){
cout<<i<<" ";
}
}
return 0;
}
/*
参考题目:https://www.luogu.com.cn/problem/P3388
*/