目录
Tarjan求强连通分量的个数,前面介绍过,在这里稍微一略过。在有向图中,寻找一个点所能到达的最小的时间戳(回溯的被遍历个数最小的编号)。对于一个结点u,那么low[u]肯定是本身++time和所有子节点low[v]的最小的一个(v可到达的,u都可以到达),更友好的是,点可以多次经过(存在重边或者两点形成环等),所以可得核心代码:
for(int i = head[u];i != -1;i = edge[i].next){
int v = edge[i].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[v],low[u]);
}
else if(vis[v]) low[u] = min(low[v],low[u]);
}
vis代表仍在栈中,也就是从栈低元素一路走过来,所以u如果可以到已访问的v(祖先节点),则必成环,必在一个强连通分量中,所以用low[v]早更新也一样。
割点:
割点:删除该点后,一个图分成了两个连通分支。意味着该点所有的子节点最多可以回溯到该割点本身,不可能不经过父节点的情况下回溯更早。
因为不可以经过父节点,所以修改的第一条:
else if(vis[v]) low[u] = min(low[v],low[u]);中的low[u] = min(low[v],low[u]);
比如v把该图分成左右两部分,v连接起来。u在右侧,如果v的low是比v要小的话,这时候u就更新为了low[v],u回溯到了左侧,但是经过了v,所以需要改为:
low[u] = min(low[u],dfn[v]);
对于根节点,如果可以遍历一遍全部完成就肯定不是割点,否则必为割点,记录下即可。
代码:
#include<algorithm>
#include<iostream>
#include<cstring>
#include<string>
#include<cstdlib>
#include<map>
#include<cmath>
#include<vector>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn = 2e5+50;
struct ed{
int to;
int next;
}edge[maxn];
int head[maxn];
int dfn[maxn],low[maxn];
bool vis[maxn];
int tme = 0,tot = 0;
void add(int u,int v){
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
void tarjan(int u,int root,int fa){
int children = 0;
dfn[u] = low[u] = ++tme;
for(int i = head[u];i != -1; i = edge[i].next){
int v = edge[i].to;
if(!dfn[v]){
tarjan(v,root,u);
if(u == root) children++;//记录回溯到根节点次数
low[u] = min(low[u],low[v]);
if(children>1&&u == root) vis[u] = true;
if(u!=root&&low[v] >= dfn[u]) vis[u] = true;
}
else if(fa!=v) low[u] = min(low[u],dfn[v]);
//与求强连通分量区别在于low[u]=min(low[u],low[v])在这里会出错。
//因为强连通分量v一定是在栈中,也就是肯定是在一个强连通分量中
//早更新晚更新都无所谓
//并且强连通分量是允许经过父节点!!! 这个不允许。
}
}
int main(){
int n,m,x,y;
cin >> n >> m;
for(int i = 1;i <= n;i++) head[i] = -1;
for(int i = 1;i <= m;i++){
cin >> x >> y;
add(x,y);
add(y,x);
}
for(int i = 1;i <= n;i++)
if(!dfn[i]) tarjan(i,i,i);
vector<int>ans;
for(int i = 1;i <= n;i++)
if(vis[i]) ans.push_back(i);
cout<<ans.size()<<endl;
//割点个数
for(int i = 0;i < ans.size();i++) cout<<ans[i]<<' ';
//割点编号
cout<<endl;
return 0;
}
割边:
割边:删除该边后,一个图变为两个连通分支。
割点是还能不经过父节点还存在回溯到父节点的情况。割边的话是根本没法回到父节点。相比割点,只需改下:if(low[v] > dfn[u])即可。
细节区别:根节点无需特判,不可用结点标记计数,因为一个结点可能连接多个割边。
代码:
#include<algorithm>
#include<iostream>
#include<cstring>
#include<string>
#include<cstdlib>
#include<map>
#include<cmath>
#include<vector>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn = 4e6+50;
struct ed{
int to;
int next;
}edge[maxn];
int ans = 0;
int head[maxn];
int dfn[maxn],low[maxn];
bool vis[maxn];
int tme = 0,tot = 0;
void add(int u,int v){
edge[tot].to = v;
edge[tot].next = head[u];
head[u] = tot++;
}
void tarjan(int u,int fa){
//割点需要记录根,特判根。割边需要记住父节点,无向图转化为双向图,这时候孩子一旦
//遍历了父节点,也跟新了父节点的dfn,就不存在>的情况了
//割点记录fa也不会错,更好,因为更新的应该是祖先节点,即不经过父节点
dfn[u] = low[u] = ++tme;
for(int i = head[u];i != -1; i = edge[i].next){
int v = edge[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u] = min(low[u],low[v]);
//不能像割点一样进行标记
//割点标记是否点是因为求割点数目
//一个点可能连多个割边,所以需要直接++,或者存点对。
if(low[v] > dfn[u]) ans++;
}
else if(fa != v) low[u] = min(low[u],dfn[v]);
}
}
int main(){
int n,m,x,y;
cin >> n >> m;
for(int i = 1;i <= n;i++) head[i] = -1;
for(int i = 1;i <= m;i++){
cin >> x >> y;
add(x,y);
add(y,x);
}
for(int i = 1;i <= n;i++)
if(!dfn[i]) tarjan(i,i);
cout<<ans<<endl;
return 0;
}