连通性&割顶&桥&双连通&强连通&tarjan。。。
hicc0305
2018-04-24 09:23:30
## 概念:
### 连通分量:
连通:在无向图中,若从Vi到Vj有路径,则称Vi和Vj是连通的,而无向图有自反性、对称性和传递性,因此此时Vj和Vi也是连通的。
#### 连通分量:
Connected Component,相互可达的结点集合。
#### 连通图:
在无向图G中,若任意两个不同的顶点都连通,则G为连通图。
### 割顶和桥:
#### 割顶:
Cut Vertex,对于无向图G,如果删除某个顶点u后,连通分量数目增加,则u便是图G的关节点(Articulation Vertex)或割顶。
#### 桥:
Bridge,如果删除某条边,G就非连通了,这条边就称之为桥。
### 时间戳:
记录全局的当前时间。每个点访问的时间戳可以表示出各个点的访问顺序。
### 反向边
第一次处理时,从后代指向祖先的边称为反向边。
![](https://cdn.luogu.com.cn/upload/pic/17983.png)
### 双连通分量:
#### 点-双连通:
对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,
则说这个图是点-双连通的(一般简称为双连通,Biconnected)
任意两条边都在同一个简单环内,即内部无割顶
#### 边-双连通:
任意两点至少存在两条“边不重复”的路径(Edge-biconnected)
每条边都至少在一个简单环中,即所有边都不是桥
#### 双连通分量:
对于无向图,点-双连通的极大子图成为双连通分量(Biconnected Component,BCC)或块(Block)
每条边恰好属于一个双连通分量
不同连通分量可能会有公共点(最多也只有一个公共点),这也就是割顶
任意一个割顶都是至少两个不同双连通分量的公共点
#### 边-双连通分量:
边-双连通的极大子图
桥不属于任何一个边-双连通分量
其他边恰好属于一个边-双连通分量
![](https://cdn.luogu.com.cn/upload/pic/17980.png)
点-双连通分量:{A,C,D}和{B,C,E}
边-双连通分量:{A,C,B,E,D}
### 强连通
强联通:在有向图中,若Vi到Vj有路径,Vj到Vi也有路径,则叫Strongly Connected Component, SCC。
强连通分量:强联通的极大子图
## TIP:双连通为无向图,强连通为有向图
------------
------------
## 程序
几个数组的概念:
pre:记录当前节点的时间戳
low:low[x]为x及其后代所能通过反向边连回的最早的祖先的pre值
则若节点u的子节点v的low[v]≤pre[u],u是割顶
若low[v]>pre[u],即v的后代只能连回v自己,那么只需删除(u,v)这条边就可以让G非连通了,这条边就是桥。当然u也是割点
### 求割顶和桥:
```cpp
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,cnt=0,tot=0,num=0;
int head[200100],nxt[200100],to[200100];
int low[100100],f[100100],pre[100100];
void addedge(int x,int y)
{
cnt++;
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
}
void dfs(int u,int fa)
{
int ch=0;
low[u]=pre[u]=++tot;
for(int i=head[u];i!=-1;i=nxt[i])
{
int v=to[i];
if(!pre[v])//若v还未访问过
{
ch++;
dfs(v,u);//继续dfs求出low[v]
low[u]=min(low[u],low[v]);
if(low[v]>=pre[u]) f[u]=1;//low[v]>=pre[u] 则为割顶
if(low[v]>pre[u]) num++;//low[v]>pre[u] 则为割顶
}
else if(pre[v]<pre[u] && v!=fa) low[u]=min(low[u],pre[v]);//v已经访问过,并且比u先访问到,如果不是u的父亲节点,low[u]就可以更新
}
if(fa==0 && ch==1) f[u]=0;//若u为根节点并且只有一个子节点把根节点去掉不会多出连通分量,所以u不为割顶
}
int main()
{
while(1)
{
memset(f,0,sizeof(f));
memset(pre,0,sizeof(pre));
memset(low,0,sizeof(low));
memset(head,-1,sizeof(head));
cnt=0,tot=0,num=0;
scanf("%d%d",&n,&m);
if(!n && !m) break;
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
addedge(x,y);
addedge(y,x);
}
for(int i=1;i<=n;i++)
if(!pre[i]) dfs(i,0);
int ans=0;
for(int i=1;i<=n;i++)
if(f[i]) ans++;
cout<<ans<<" "<<num<<endl;//ans是割顶数量,num是桥的数量
for(int i=1;i<=n;i++)
if(f[i]) cout<<i<<endl;
}
return 0;
}
```
### 求双联通分量
代码其实差不多,这种方法求双联通分量叫做tarjan
```cpp
int bcc_cnt; //点_双连通分量的数目
int belong[maxn]; //节点属于的点_双连通分量的编号
vector<int> G[maxn], bcc[maxn]; //点_双连通分量
int tarjan(int u, int fa)
{
int lowu = pre[u] = ++dfs_clock, child = 0;
for (int i = 0; i < G[u].size(); i++)
{
int v = G[u][i]; Edge e = Edge(u, v);
if (!pre[v])
{
s.push(e); child++;
int lowv = tarjan(v, u); lowu = min(lowu, lowv); //用后代更新lowu
if(lowv >= pre[u])
{
iscut[u] = true; bcc_cnt++; bcc[bcc_cnt].clear();
while(1)
{
Edge x = s.top(); s.pop();
if (belong[x.u] != bcc_cnt)
{
bcc[bcc_cnt].push_back(x.u);//x.u在第bbc_cnt个双连通分量中
belong[x.u] = bcc_cnt;//x.u所在的双连通分量的编号是bcc_cnt
}
if (belong[x.v] != bcc_cnt)
{
bcc[bcc_cnt].push_back(x.v);
belong[x.v] = bcc_cnt;
}
if (x.u == u && x.v == v) break;
}
}
}
else if (pre[v] < pre[u] && v != fa)
{
s.push(e);
lowu = min(lowu,pre[v]);
}
}
if (fa < 0 && child == 1) iscut[u] = 0;
return lowu;
}
```
### 求强联通分量
代码其实也差不多一样,而且算法也叫tarjan。。。总之Tarjan是个很六的人
```cpp
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,ans=0;
int sta[200001],top=0;
int dfn[200001],low[200001],tot=0;
int head[200001],nxt[200001],to[200001],cnt=0;
int f[200001];
void addedge(int x,int y)//邻接链表存图
{
cnt++;
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
}
void tarjan(int u)//tarjan求强联通分量
{
f[u]=1;
dfn[u]=low[u]=++tot;
sta[++top]=u;//入栈
for(int i=head[u];i!=0;i=nxt[i])
{
int v=to[i];
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(f[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
ans++;
do
{
f[sta[top]]=0;
}while(sta[top--]!=u);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int a;
scanf("%d",&a);
addedge(i,a);//加边,是有向边,别加两次
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
cout<<ans;//强连通分量的数量
return 0;
}
```