求点双联通分量
算法流程如下:
- 建立一个栈,存储 D F S DFS DFS过程中访问的节点,初次访问一个点时把该点入栈。
- 如果边
(
u
,
v
)
(u,v)
(u,v)满足
low[v]
≥ \geq ≥dfn[u]
,即满足了u是割点的判断条件,那么把点从栈顶依次取出,直至取出了点 v v v,取出的这些点和点 u u u一起组成一个点双连通分量。 - 割点可能属于多个点双连通分量,其余点和每条边属于且仅属于一个点双连通分量。因此在从栈中取出节点时,要把 u u u留在栈中。
- 整个 D F S DFS DFS结束后,栈中还剩余的节点构成一个点双连通分量。
可以发现的是, D F S DFS DFS到的所有的点一定强连通。因为它们被 D F S DFS DFS的“树边”连接起来。
任意一次 D F S DFS DFS完一个割点后,删去这个割点,一定不影响剩余部分(除去已经被“弹栈”的点)的联通性。假如剩余部分本来就不连通,那么删去割点之后也一定不连通。假如剩余部分强连通,对于发现的某个割点,割点为根的子树上残余的节点并且必然在去除割点后与原图中除割点为根的子树的节点的部分连通(否则就满足割点的条件,那么也将被“弹栈”)。而此后加入的所有节点必然不在以割点为根的子树上,所以删去割点不影响其连通性。
除去从前往后若干次“弹栈”的点,剩余部分仍然强连通。对于第一次“弹栈”,此结论成立,因为弹出的只是一棵子树。若某次”弹栈“操作先前的若干次“弹栈”之后剩余部分仍然强连通,那么对于这次操作,此结论成立,也因为弹出的只是一棵子树。
我们先证明第一次由算法得到的点集是点双联通分量,并且随后所有已经属于某个点双连通分量的非割点的点都被弹出栈中。假设它们构成的子图为 G G G。首先要证明 G G G是点双联通子图。假设算法第一次得到的割点为 u u u,删去除割点外的任意一点, G G G中剩余的所有点必然也强连通(不然这个点就是割点了)。算法得到的是一棵子树,所以说删去割点后,剩余的部分也仍然强连通。因此 G G G是点双连通子图。接下来证明 G G G是“极大”的。假如说 G G G可以加入割点为根的子树以外的点,那么删去割点之后就不再强连通。同理,加入以割点为根的子树中除 G G G以外的点,删去割点后就不再强连通(因为无向图中没有横叉边)。所以说,第一次得到的是点双连通分量,所有已经属于某个点双连通分量的非割点的点都被弹出栈中。
再来证明如果第
n
n
n次成立,那么第
n
+
1
n+1
n+1次也成立。第
n
+
1
n+1
n+1次得到的子图必然是点双连通子图,因为删去割点,剩余部分一定强连通(因为在一棵子树上)。而删去除去割点外的任意一点,此图仍然强连通。如果是“普通”点,那删去没有影响。如果是之前发现的割点,由之前的结论我们可以知道删去之后也不影响连通性(就是剩余部分仍然强连通),那么这是点双连通子图。割点必然有对应的点
v
v
v使得
v
v
v的low
大于等于割点的dfn
。对于
v
v
v为根的子树上已经”弹栈“的点,必然不属于这个点双连通分量。对于除去
v
v
v为根的子树和割点外的一点,如果这个点在割点为根的子树以外,那么删去割点后
v
v
v为根的子树与这个点就不再连通。如果说这个点在割点为根的子树上,那么必然不在
v
v
v为根的子树上,那么删去割点后
v
v
v为根的子树就不再与这个点连通(因为无向图中没有横叉边)。所以说,第
n
+
1
n+1
n+1次得到的时点双连通分量,所有已经属于某个点双连通分量的非割点的点都被弹出栈中。
如果栈中有剩余的节点,那么所有的割点都已经被 D F S DFS DFS完了,也就是说删去栈中剩余的不论是原图的割点还是非割点,栈中剩余的点都仍然强连通。所以,剩余的点构成强连通子图。而弹出栈的所有点都必然不属于这个强连通分量,所以剩余的点单独构成一个强连通分量。
洛谷模板题参考代码:
#include<cstdio>
#include<vector>
#define f_inline inline __attribute__((always_inline))
using namespace std;
using UL=unsigned long;
UL tot,head[500005],cnt,col,dfn[500005],low[500005],root;
vector<UL> rem[500005];
struct Arc
{
UL to,nex;
}arc[4000005];
struct Stack
{
UL sz,arr[500005];
f_inline void emplace(register const UL x)
{
arr[++sz]=x;
}
f_inline UL top()
{
return arr[sz];
}
f_inline void pop()
{
--sz;
}
f_inline UL size()
{
return sz;
}
}sta;
f_inline void Add(register const UL u,register const UL v)
{
arc[++tot].nex=head[u],
head[u]=tot,
arc[tot].to=v;
}
f_inline void updMin(register UL &sou,register const UL x)
{
if(x<sou)
{
sou=x;
}
}
void tarjan(register const UL u)
{
dfn[u]=low[u]=++cnt,
sta.emplace(u);
register UL son(0);
for(register UL i(head[u]);i;i=arc[i].nex)
{
register const UL to(arc[i].to);
if(!dfn[to])
{
tarjan(to),
updMin(low[u],low[to]);
if((u==root&&++son>1)||(u!=root&&low[to]>=dfn[u]))//满足割点条件
{
++col;
while(sta.top()!=to)
rem[col].emplace_back(sta.top()),
sta.pop();
rem[col].emplace_back(sta.top()),
sta.pop(),
rem[col].emplace_back(u);
}
}
else
{
updMin(low[u],dfn[to]);
}
}
}
int main()
{
register UL n,m;
scanf("%lu%lu",&n,&m);
for(register UL i(1);i<=m;++i)
{
register UL u,v;
scanf("%lu%lu",&u,&v),
Add(u,v),
Add(v,u);
}
for(register UL i(1);i<=n;++i)
if(!dfn[i])
{
tarjan(root=i);
if(sta.size())
{
++col;
while(sta.size())
rem[col].emplace_back(sta.top()),
sta.pop();
}
}
printf("%lu\n",col);
for(register UL i(1);i<=col;++i)
{
printf("%lu ",rem[i].size());
for(register vector<UL>::iterator it(rem[i].begin());it!=rem[i].end();++it)
printf("%lu ",*it);
putchar('\n');
}
return 0;
}
求边双连通分量
算法思路如下:
- 在求出所有的桥以后,把桥删除。
- 此时原图分成了若干个连通块,每个连通块就是一个边双连通分量。
- 桥不属于任何一个边双连通分量。
- 其余的边和每个顶点都属于且仅属于一个边双连通分量。(如果属于两个或更多,那么这些边双连通分量就可以合并成一个更大的边双连通子图,与”分量“的条件矛盾)
删除所有桥之后的任意连通块中的任意两个点 u u u、 v v v都必然满足其间存在至少两条边不重复路径,因为在删除所有桥之前这两个点之间就存在至少两条边不重复路径(不然删去桥之后它们就不连通了),而它们和路径上的所有点必然在删去桥后仍然在同一个强连通分量中(如果路径上存在一条边使得这是某两个点的必经边,那么从边的一个端点到达 u u u之后经过一条边不重复路径到达 v v v,然后再通过这条路径到达边的另一个端点就可以取代这条必经边,所以路径上必然不存在桥)。所以删去所有的桥后每一个连通块都是一个边双连通子图,而对于这个子图外的点和这个子图,它们必然不强连通,所以每一个连通块都是一个边双连通分量。
洛谷模板题参考代码:
#include<cstdio>
#include<vector>
#include<bitset>
#include<cstring>
#define f_inline inline __attribute__((always_inline))
using namespace std;
using UL=unsigned long;
UL tot,head[500005],dfn[500005],low[500005],cnt,col;
vector<UL> rem[500005];
bitset<4000005> isBri;//默认全部不是
bitset<500005> vis;
struct Arc
{
UL to,nex;
}arc[4000005];
f_inline void Add(register const UL u,register const UL v)
{
arc[++tot].nex=head[u],
head[u]=tot,
arc[tot].to=v;
}
f_inline void updMin(register UL &sou,register const UL x)
{
if(x<sou)
{
sou=x;
}
}
f_inline UL par(register const UL x)//返回x的反向边的编号
{
return (x&1)?(x+1):(x-1);
}
void Tarjan(register const UL u,register const UL fa)
{
dfn[u]=low[u]=++cnt;
register bool flag(false);//flag<-->当前可以回到父节点
for(register UL i(head[u]);i;i=arc[i].nex)
{
register const UL to(arc[i].to);
if(!dfn[to])
{
Tarjan(to,u);
updMin(low[u],low[to]);
if(low[to]>dfn[u])
{
isBri.set(i),
isBri.set(par(i));
}
}
else
{
if(to==fa)
{
flag?updMin(low[u],dfn[to]):static_cast<void>(flag=true);
}
else
{
updMin(low[u],dfn[to]);
}
}
}
}
void dfs(register const UL u)
{
vis.set(u);
for(register UL i(head[u]);i;i=arc[i].nex)
{
if(isBri.test(i))
{
continue;
}
register const UL to(arc[i].to);
if(!vis.test(to))
{
dfs(to);
}
}
rem[col].emplace_back(u);
}
int main()
{
register UL n,m;
scanf("%lu%lu",&n,&m);
for(register UL i(1);i<=m;++i)
{
register UL u,v;
scanf("%lu%lu",&u,&v),
Add(u,v),
Add(v,u);
}
for(register UL i(1);i<=n;++i)
if(!dfn[i])
{
Tarjan(i,0);
}
for(register UL i(1);i<=n;++i)
if(!vis.test(i))
{
++col,
dfs(i);
}
printf("%lu\n",col);
for(register UL i(1);i<=col;++i)
{
printf("%lu ",rem[i].size());
for(register vector<UL>::iterator it(rem[i].begin());it!=rem[i].end();++it)
printf("%lu ",*it);
putchar('\n');
}
return 0;
}