【模板】Tarjan求强连通分量/求割点/求割边
codesonic
2018-02-05 17:29:43
![](https://cdn.luogu.com.cn/upload/pic/14613.png)
# 强连通分量
先下几个定义:
强连通:在一个有向图G中,节点a可以到达节点b,节点b也可以到达节点a,则这两个节点强连通
强连通图:在一个有向图G中,任意两个节点都能互相到达,则这个图是强连通图
强连通分量:在一个有向图G中,有子图满足每2个点都强连通,则这个子图是强连通分量
只有一个节点的图为强连通图。
拿一张在全网满天飞的图做样例:
![](https://cdn.luogu.com.cn/upload/pic/14610.png)
图中{1,3,4,2}、{5}、{6}分别为一个强连通分量
即:
![](https://cdn.luogu.com.cn/upload/pic/14608.png)
上图由红线框起来的为强连通分量
---
Tarjan是基于DFS的算法,利用DFS构造一个搜索树,每个强连通分量为搜索树上的一个子树。搜索时把每个节点~~扔~~压进一个一个栈,回溯时判定。
对节点i定义两个数组元素,DFN[i]和LOW[i],分别表示节点i是第几个被搜索到的节点,LOW[i]为节点i或节点i的子树能回溯到的最早的栈中节点的DFN
则
LOW[i]=min(DFN[i],所有LOW[j](i为j的父节点),DFN[j](j为i的父节点且j在栈中))
显然如果DFN[i]=LOW[i],则i为目前搜索树的根
给段伪代码吧,结合代码理解LOW[i]的赋值
```cpp
void tarjan(int i) {
搜索次序编号自增1
DFN[i]=LOW[i]=搜索次序编号;
将i压入栈;
for(对i的每条出边) {
定义j为遍历到的出边的终点
if (j没有被搜索过) {
搜索j;
LOW[i]=min(LOW[i],LOW[j]);
}
else if(j在栈中)
LOW[i]=min(LOW[i],DFN[j]);
}
if (i为目前搜索树的根) {
将所有以i为祖先的栈中节点出栈
}
}
```
和其他的blog一样,对下面这张图演示一下tarjan算法的流程
![](https://cdn.luogu.com.cn/upload/pic/14610.png)
搜索第一个节点:
DFN[1]=LOW[1]=1,1入栈
因为3没有被搜索过,所以3入栈,搜索3,再对5遍历,5仍没有被搜索过,搜索5,接着搜索6,此时栈的情况如下图:
![](https://cdn.luogu.com.cn/upload/pic/14607.png)
且DFN[1]=1,DFN[3]=2,DFN[5]=3,DFN[6]=4,LOW[6]=4,发现6没有出边,开始退栈,仅退了6一个节点,所以{6}为一个强连通分量,返回5的搜索,同理把5退栈,{5}为一个强连通分量
返回节点3的搜索,遍历了节点4,DFN[4]=5,发现节点4有出边去1,1在栈中且1的的DFN为1,所以LOW[4]=1,继续搜索节点4,发现节点4有边连向6,但6不在栈内且6已经被搜索过,不管。返回节点3的搜索
节点3搜索结束,返回节点1的搜索
节点1有出边到达2,搜索节点2,DFN[2]=6,节点2有出边到4,4已经搜索过但,4在栈中,LOW[2]=DFN[4]=5,返回节点1的搜索
发现节点DFN[1]=LOW[1],退栈,取出了栈中所有节点,得出最后一个强连通分量{1,3,4,2}
搜索至此结束
但是如果这个图G不联通呢?
很简单,在主函数调用tarjan的地方加个for就行了:
```cpp
for(int i=1;i<=n;i++)
if(!DFN[i])
tarjan(i);
```
好像也没有全裸的模版题,比较裸的只能给出这个了[P2835](https://www.luogu.org/problemnew/show/P2835)
以下是该题代码,链式前向星存图:
```cpp
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=10010,M=50010;
int n;
int tot=0,head[M],head2[M],head3[M],cnt,Index,top;//cnt是强连通分量的个数,Index是是搜索次序编号,head2,head3计数用
int DFN[N],LOW[N],stap[N],which[N];//stap是栈,which表示该节点位于哪一个强连通分量,
bool flag[N][N];
bool instack[N];
struct edge {
int u,v,next;
} G[M];
void add_edge(int u,int v) {
G[++tot].u=u;
G[tot].v=v;
G[tot].next=head[u];
head[u]=tot;
}
void tarjan(int i) {
DFN[i]=LOW[i]=++Index;
instack[i]=true;
stap[++top]=i;
for (int e=head[i]; e; e=G[e].next) {
int j=G[e].v;
if (!DFN[j]) {
tarjan(j);
LOW[i]=min(LOW[i],LOW[j]);
}
else if(instack[j])
LOW[i]=min(LOW[i],DFN[j]);
}
int x;
if (DFN[i]==LOW[i]) {
cnt++;
do {
x=stap[top--];
instack[x]=false;
which[x]=cnt;
} while (x!=i);
}
}
int main() {
scanf("%d",&n);
for(int i=1; i<=n; i++) {
while(1) {
int x;
scanf("%d",&x);
if(!x) break;
add_edge(i,x);
}
}
for(int i=1; i<=n; i++)
if(!DFN[i])
tarjan(i);
for(int i=1; i<=n; i++) {
for(int j=head[i]; j; j=G[j].next) {
if(which[i]!=which[G[j].v]) {
head2[which[i]]++;
head3[which[G[j].v]]++;
}
}
}
int num=0;
for(int i=1; i<=cnt; i++) {
if(!head3[i])
num++;
}
printf("%d\n",num);
return 0;
}
```
分析一下时间复杂度
其中tarjan函数显然要遍历每一条边和每一个点的时间复杂度是O(n+m)
以下的代码并没有产生更多的时间复杂度,因为它仅对没有搜索过的点进行搜索
```cpp
for(int i=1; i<=n; i++)
if(!DFN[i])
tarjan(i);
```
---
---
---
# 割点
割点也是可以用tarjan算法求的~~(tarjan老爷子你到底发明了多少算法)~~
在求割点的代码中,DFN和LOW的含义一样,可以不用多说,主要要写的是割点的定义和如何求。
先引入概念“双联通分量”,双联通分量的定义为:
在一个双联通分量中,删去任何一个点(即删除它与连接着它的所有边)之后这个分量仍然强连通
割点的定义为:
在一个强连通分量中,删去了某一个点之后这个强连通分量不再强连通,则称这个点为割点。
显然双联通分量是没有割点的
割点的判断方法如下(可以尝试证明):
如果这个点u是搜索树的树根,且它的子树数量大于或等于二,那么u为割点;
如果有一条边(u,v),其中u在搜索树中是v的父亲,且DFN(u)<=LOW(v),则点u为割点。
那么代码很显然了。
例题:[P3388](https://www.luogu.org/problemnew/show/P3388)
代码如下:
```cpp
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1000010,M=1000010;
int n,m;
int tot=0,head[M],head2[M],head3[M],cnt,Index,top;
int DFN[N],LOW[N];
bool is[N];
bool instack[N];
struct edge {
int u,v,next;
} G[M];
void add_edge(int u,int v) {
G[++tot].u=u;
G[tot].v=v;
G[tot].next=head[u];
head[u]=tot;
}
void tarjan(int u,int fa) {
int son=0;
DFN[u]=LOW[u]=++Index;
for (int i=head[u]; i; i=G[i].next) {
int v=G[i].v;
if(!DFN[v]) {
tarjan(v,fa);
LOW[u]=min(LOW[u],LOW[v]);//定义
if(u==fa) son++;
else if(LOW[v]>=DFN[u]) is[u]=1;
}
LOW[u]=min(LOW[u],DFN[v]);
}
if(son>=2) is[u]=1;
}
int main() {
scanf("%d%d",&n, &m);
for(int i=1; i<=m; i++) {
int x,y;
scanf("%d%d",&x,&y);
add_edge(y,x);
add_edge(x,y);
}
for(int i=1; i<=n; i++)
if(!DFN[i])
tarjan(i,i);
int ans=0;
for (int i=1; i<=n; i++)
if(is[i])
ans++;
printf("%d\n",ans);
for(int i=1; i<=n; i++)
if(is[i])
printf("%d ",i);
return 0;
}
```
# 割边(桥)
桥其实就是割边
![](https://cdn.luogu.com.cn/upload/pic/16259.png)
我们还是利用low和dfn的性质作文章
~~(其实这些东西没几个人会证)~~
对于一条边(u,v),若满足$LOW[v]>DFN[u]$,那么这条边是割边
这个时候我们悲惨地发现,会发生重边
那么我们怎么处理呢?
我们直接在给边多维护一个值,加边的时候判断有没有重边就行了
于是addedge函数变成了这样:
```cpp
void add_edge(int u,int v,int id) {
for(int i=head[u];i;i=G[i].next)
{
if(G[i].v==v)
{
G[i].mul=true;
return;
}
}//找重边
G[tot].id=id;
G[tot].v=v;
G[tot].next=head[u];
head[u]=tot++;
G[tot].id=id;
G[tot].v=u;
G[tot].next=head[v];
head[v]=tot++;
}
```
(因为是无向图要存两次边,无法确认边的id,对每一条边引入了新的一个变量id)
tarjan函数也差不离:
```cpp
void tarjan(int u,int fa) {
DFN[u]=LOW[u]=++Index;
for (int i=head[u]; i; i=G[i].next) {
int v=G[i].v;
if(!DFN[v]){
tarjan(v,u);
LOW[u]=min(LOW[u],LOW[v]);
if(LOW[v]>DFN[u]&&!G[i].mul)
is[G[i].id]=1;
}
else if(v!=fa)
LOW[u]=min(LOW[u],DFN[v]);
}
}
```
(其中v!=fa是因为不是父亲边才更新LOW)
因为没有例题所以自己创了一道私题:
[割点](https://www.luogu.org/problemnew/show/U22159)