割点&&桥&&边双&&点双
定义:
割点:将原图中的某一点以及它所连的边删除后,原图不连通。
桥:将原图中的某一边删除后,原图不连通。
边双连通分量:原图中意删除一边后还连通的极大连通子图。
点双连通分量:原图中任意删除一点后还连通的极大连通子图。
求法:
割点:
考虑原图的 dfs 生成树,对于树边更新 : \(low[u]=\min(low[u],low[v])\),对于非树边 \(low[u]=\min(low[u],dfn[v])\)
关键点:假如对于 \(u\) 的子节点 \(v\) 有 \(low[v] \ge dfn[u]\),那么 \(u\) 为割点
为什么呢?原因就在于 \(low\) 的定义是能够到达的节点最早的时间戳,那么假如 \(low[u] \ge dfn[v]\) ,就代表 \(u\) 实际上并不能通过别的节点来到 \(u\) 以上的节点,就代表假如割掉了 \(u\) ,\(v\) 就不能到达 \(u\) 以上的节点了,所以割掉了 \(u\) ,\(v\) 就寄了。
代码:
void tarjan(int u,int fa){
dfn[u]=low[u]=++tim;
int child=0;
for(int i=head[u];i;i=edges[i].next){
int v=edges[i].v;
if(!dfn[v]){
child++;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(fa!=-1&&low[v]>=dfn[u]){
cut[u]=true;
}
}
else if(dfn[v]<dfn[u]&&v!=fa){
low[u]=min(low[u],dfn[v]);
}
}
if(fa==-1&&child>=2){
cut[u]=true;//这里注意 u 为根节点的情况
}
return;
}
桥:
还是一样,使用 tarjan 求解。
这里有一个性质:\(low[v]>dfn[u]\),\(u\) 与 \(v\) 连的边就是一个桥了。
证明同上。
代码:
#include<bits/stdc++.h>
#define L long long
using namespace std;
const int mod=1000000007,N=1145;
L pre[N],dfn[N],low[N],n,m,dt,ans;
bool vis[N];
vector<L> G[N];
struct edge{
L from,to;
}a[N];
inline int read()
{
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void dfs(L fa ,L now){
dfn[now]=low[now]=++dt;
L len=G[now].size();
for(L i=0;i<len;i++){
L next=G[now][i];
if(next!=fa&&dfn[next]) low[now]=min(low[now],dfn[next]);
if(!dfn[next]){
vis[now]=false;
dfs(now,next);
if(dfn[now]<low[next]){
a[++ans].from=min(now,next);
a[ans].to=max(now,next);
}
low[now]=min(low[now],low[next]);
}
}
}
bool cmp(edge p,edge q){
if(p.from!=q.from) return p.from<q.from;
return p.to<q.to;
}
int main(){
n=read(),m=read();
memset(vis,true,sizeof(vis));
for(L i=1;i<=m;i++){
L u=read(),v=read();
G[v].push_back(u);
G[u].push_back(v);
}
for(L i=1;i<=n;i++) if(!dfn[i]) dfs(i,i);
sort(a+1,a+ans+1,cmp);
for(L i=1;i<=ans;i++){
printf("%lld %lld\n",a[i].from,a[i].to);
}
return 0;
}
边双:
与强连通分量一样,当遍历完 \(u\) 的所有子节点后,若 \(low[u]=dfn[u]\),上面的都是边双的元素。
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+10;
struct edge{
int v,next;
}edges[N*10];
int head[N],idx=2;
int dfn[N],low[N],clk;
int st[N],top;
bool Cut[N*10];
vector<int>ans[N];
int edcc;
void add_edge(int u,int v){
idx++;
edges[idx].v=v;
edges[idx].next=head[u];
head[u]=idx;
return;
}
void tarjan(int u,int fa){
dfn[u]=low[u]=++clk;
st[++top]=u;
for(int i=head[u];i;i=edges[i].next){
int v=edges[i].v;
if(v==fa)continue;
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]){
Cut[i]=Cut[i^1]=true;
}
}
else{
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
edcc++;
do{
ans[edcc].push_back(st[top]);
}while(st[top--]!=u);
}
return;
}
signed main(){
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
add_edge(x,y);
add_edge(y,x);
}
tarjan(1,-1);
cout<<edcc<<endl;
return 0;
}
点双:
点双即是找到割点后,将割点上的所有元素出栈,并加入这个点双中(包括割点也要加入),但不要将割点出栈(因为有可能割点属于多个点双)。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
struct edge{
int v,next;
}edges[N];
int head[N],idx;
int n,m;
int low[N],dfn[N],clk;
vector<int>ans[N];
int st[N],top;
bool Cut[N];
int vdcc,root;
void add_edge(int u,int v){
idx++;
edges[idx].v=v;
edges[idx].next=head[u];
head[u]=idx;
return;
}
void tarjan(int u){
dfn[u]=low[u]=++clk;
st[++top]=u;
if(u==root&&(!head[u])){
vdcc++;
ans[vdcc].push_back(u);
return;
}
int child=0;
for(int i=head[u];i;i=edges[i].next){
int v=edges[i].v;
if(!dfn[v]){
tarjan(v);
child++;
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
if(u!=root||child>1)Cut[u]=true;
vdcc++;
int tmp;
do{
tmp=st[top--];
ans[vdcc].push_back(tmp);
}while(tmp!=v);
ans[vdcc].push_back(u);
}
}
else low[u]=min(low[u],dfn[v]);
}
return;
}
signed main(){
std::ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
if(x==y)continue;
add_edge(x,y);
add_edge(y,x);
}
for(int i=1;i<=n;i++)if(!dfn[i]){
root=i;
tarjan(i);
}
cout<<vdcc<<endl;
return 0;
}