史上代码最简单,讲解最清晰的双连通分量
(需提前学习强连通分量)
双连通分量的主要内容包括割点、桥(割边)、点双和边双,分别对应 4 个 Tarjan 算法。
所有算法的时间复杂度均为 O(n + m)。
双连通分量用到 DFS 树的性质,所有的边分别树边和返祖边两类,大大简化了代码。
双连通分量具有大量的性质,要能熟练掌握。
一些定义:树枝边:DFS时经过的边(由上至下);
返祖边:与DFS方向相反,从某个节点指向某个祖先的边;
返祖边:与DFS方向相反,从某个节点指向某个祖先的边;
注意:在无向图中,不能用dfn[fa]更新low[u];所以我们需要标记fa;
但如果有重边,就可以;所以我们可以记录它的上一条边;利用成对储存的思想记录上一条边来判重;
求割点:
割点性质:
(1)根结点如果是割点当且仅当其子节点数大于等于 2;
(2)非根节点 u 如果是割点,当且仅当存在 u 的一个子树,子树中没有连向 u 的祖先的边(返祖边)。
(2)非根节点 u 如果是割点,当且仅当存在 u 的一个子树,子树中没有连向 u 的祖先的边(返祖边)。
代码:
void tarjan(int u,int fa) //当fa=0时,说明该节点是根节点;
{
int num=0; //用来计量子节点数;
low[u]=dfn[u]=++cur;
for(int i=head[u];i;i=star[i].to){ //链式前向星存图;
int v=star[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(!fa && ++num>1||fa && dfn[u]<=low[v]){
//1.根节点是割点,且子节点数大于等于2;
//2.非根节点是割点,且子节点中没有返祖边;
cutpoint[u]=1; //标记该点为一个割点;
}
}
else if(v!=fa){
low[u]=min(low[u],dfn[v]);
}
}
}
求点双连通分量:
以下 3 条等价(均可作为点双连通图的定义):
(1)该连通图的任意两条边存在一个包含这两条边的简单环;
(2)该连通图没有割点;
(3)对于至少3个点的图,若任意两点有至少两条点不重复路径。
下面两句话看不看的懂都行:
点双连通分量构成对所有边集的一个划分。
两个点双连通分量最多只有一个公共点,且必为割点。进一步地,所有点双与割点可抽象为一棵树结构。
#include <bits/stdc++.h>
using namespace std;
struct littlestar{
int to;
int nxt;
}star[200010];
int head[200010],cnt;
void add(int u,int v){
star[++cnt].to=v;
star[cnt].nxt=head[u];
head[u]=cnt;
}
int low[20010],dfn[20010],cur;
pair<int,int> st[200010];
int Top,num;
vector<int> res[20010];
void tarjan(int u,int fa)
{
low[u]=dfn[u]=++cur;
for(int i=head[u];i;i=star[i].nxt){ //链式前向星存图
int v=star[i].to;
int top=Top;
if(v!=fa && dfn[u]>dfn[v]){
st[++Top]=make_pair(u,v); //当这条边并不是通往父亲的边时,并且该点的子
//树中没有返祖边时,将这条边压入栈;
}
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
++num; //num表示第几个点双区域(一个图可能存在多个点双)
for(;Top>top;Top--){ //类似于强连通分量的退栈过程;
int x=st[Top].first;
int y=st[Top].second;
if(res[x].empty() || res[x].back()!=num){
res[x].push_back(num); //由于num递增,所以res[]递增,所以res[x]的最后
//如果不是num,就代表之前不会标记过该点;
}
if(res[y].empty() || res[y].back()!=num){
res[y].push_back(num); //与上面的同理;
}
}
}
}
else if(v!=fa){
low[u]=min(low[u],dfn[v]);
}
}
}
求桥:
桥的性质: (u; v)边在dfs 树中。不妨设u 为v 的父亲,v 的子树没有向u 或其祖先连的边。
void tarjan(int u,int fa)
{
bool flag=0; //用来判断是否存在重边
low[u]=dfn[u]=++cur;
for(int i=head[u];i;i=star[i].nxt){
int v=star[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[v]==low[v]) //它的子节点v的子树中,没有像u或其祖先连的边(返祖边)
{
bridge.push_back(i); //桥的一个集合
}
}
else if(v!=fa || flag){
low[u]=min(low[u],dfn[v]);
}
else flag=1;
}
}
求边双连通分量
以下3 条等价(均可作为边双连通图的定义):
(1)该连通图的任意一条边存在一个包含这条边的简单环;
(2)该连通图没有桥;
(3)该连通图任意两点有至少两条边不重复路径。
下面两句话看不看的懂都行:
(1)边双连通分量构成对所有点集的一个划分。
(2)两个边双连通分量最多只有一条边,且必为桥。进一步地,所有边双与桥可抽象为一棵树结构。
#include <bits/stdc++.h>
using namespace std;
struct littlestar{
int to;
int nxt;
}star[10010];
int head[10010],cnt;
void add(int u,int v){
star[++cnt].to=v;
star[cnt].nxt=head[u];
head[u]=cnt;
}
int st[5010],Top,num;
int low[5010],dfn[5010],cur;
int res[5010];
int kk[150][150];
int anss[5001];
void tarjan(int u,int fa)
{
bool flag=0;
low[u]=dfn[u]=++cur;
st[++Top]=u;
for(int i=head[u];i;i=star[i].nxt){
int v=star[i].to;
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
}
else if(v!=fa || flag){
low[u]=min(low[u],dfn[v]);
}
else flag=1;
} //到此为止与求桥的意义差不多
if(low[u]==dfn[u]){ //u的子树中,没有返祖边
num++;
int tmp;
do{
tmp=st[Top--]; //退栈,原来栈中的元素构成一个边双
res[tmp]=num;
}while(tmp!=u);
}
}
来源:oschina
链接:https://my.oschina.net/u/4293666/blog/3494950