Tarjan
算法:
tarjan算法中比较重要的是
d
f
n
dfn
dfn和
l
o
w
low
low数组。
d
f
n
dfn
dfn是时间戳,或者理解是dfs序。
l
o
w
low
low是所谓的追溯值。先令
l
o
w
[
x
]
=
d
f
n
[
x
]
low[x]=dfn[x]
low[x]=dfn[x]。
现在有一条边
(
x
,
y
)
(x,y)
(x,y),如果
x
x
x是
y
y
y的父节点,
l
o
w
[
x
]
=
min
(
l
o
w
[
x
]
,
l
o
w
[
y
]
)
low[x]=\min(low[x],low[y])
low[x]=min(low[x],low[y]);否则
l
o
w
[
x
]
=
min
(
l
o
w
[
x
]
,
d
f
n
[
y
]
)
low[x]=\min(low[x],dfn[y])
low[x]=min(low[x],dfn[y])。
感觉
l
o
w
low
low是比较难以理解的点。可以理解为一个点及其子树上的点不通过和它父亲的边可以到达
d
f
n
dfn
dfn最小的点。或者说一个点不通过其父亲结点能够访问到的最早的祖先结点的时间戳。
割点:
在无向连通图中,如果将一个点和它的边去掉,图不再联通,那么称这个点为割点。
寻找割点的方法就是,对于根结点,如果它的子树数量大于2,它就是割点。对于非根结点,如果存在边
(
x
,
y
)
(x,y)
(x,y),且
d
f
n
[
x
]
≤
l
o
w
[
y
]
dfn[x]\le low[y]
dfn[x]≤low[y],那么
x
x
x就是割点。
因为如果
d
f
n
[
x
]
>
l
o
w
[
y
]
dfn[x]> low[y]
dfn[x]>low[y],那么
y
y
y结点就存在一条不通过
x
x
x的返祖边,这样割掉
x
x
x,
y
y
y依旧和祖先的点连通。反之,如果
d
f
n
[
x
]
≤
l
o
w
[
y
]
dfn[x]\le low[y]
dfn[x]≤low[y],那么
y
y
y就不可能和
x
x
x的祖先连通,这样割掉
x
x
x点,就会使图不在连通。(等于也是成立的,因为即使
y
y
y可以返回
x
x
x,割掉
x
x
x依然会使图不连通。)
代码:
void tarjan(int x,int fa)
{
dfn[x]=low[x]=++num;
int i,subt=0;//子树个数
for(i=head[x];i;i=e[i].u)
{
if(i==(fa^1))
continue;
int y=e[i].v;
if(!dfn[y])
{
subt++;
tarjan(y,i);
low[x]=min(low[x],low[y]);
if((x==root && subt>1) || (x!=root && dfn[x]<=low[y]))
p[x]=1;
}
else
low[x]=min(low[x],dfn[y]);
}
}
模板题:洛谷P3388
割边(桥):
如果连通图删去一个边
(
x
,
y
)
(x,y)
(x,y)之后,如果图不连通,那么称边
(
x
,
y
)
(x,y)
(x,y)为割边(桥)。
求法和割点相似,只是条件改成
d
f
n
[
x
]
<
l
o
w
[
y
]
dfn[x]< low[y]
dfn[x]<low[y],因为如果
y
y
y可以从另一条边返回
x
x
x,那么这条边就不是割边。
有一条语句if(i==(fa^1)) continue;
表示如果
y
y
y使通过边
(
x
,
y
)
(x,y)
(x,y)达到的,那么不走
(
y
,
x
)
(y,x)
(y,x)这条边(无向图双向建边)。如果走这条边,那么更新使得
l
o
w
[
y
]
=
d
f
n
[
x
]
low[y]=dfn[x]
low[y]=dfn[x],这个在割点的时候似乎没有什么影响,但是在割边的时候就表示存在一条非搜索树的边能够从
y
y
y返回
x
x
x,这样这个边就不能割了。
存边的时候,应该从下标2开始存,这样一条边的两个序号
a
,
b
a,b
a,b满足
a
⊕
1
=
b
,
b
⊕
1
=
a
a\oplus1=b,b\oplus1=a
a⊕1=b,b⊕1=a(
⊕
\oplus
⊕表示异或)。证明很容易,如果一个数是偶数,那么它二进制表示是
(
a
1
a
2
a
3
⋯
0
)
2
(a_1a_2a_3\cdots0)_2
(a1a2a3⋯0)2,
(
a
1
a
2
a
3
⋯
0
)
⊕
(
000
⋯
1
)
=
(
a
1
a
2
a
3
⋯
1
)
(a_1a_2a_3\cdots0)\oplus(000\cdots1)=(a_1a_2a_3\cdots1)
(a1a2a3⋯0)⊕(000⋯1)=(a1a2a3⋯1)那么它就相当于加了
1
1
1,奇数同理。这也是网络流建反向边的方法。
代码:
//求割边
void tarjan(int x,int fa)
{
dfn[x]=low[x]=++num;
int i;
for(i=head[x];i;i=e[i].u)
{
if(i==(fa^1))
continue;
int y=e[i].v;
if(!dfn[y])
{
tarjan(y,i);
low[x]=min(low[x],low[y]);
if(dfn[x]<low[y])
{
ans++;
f[i]=f[i^1]=1;
}
}
else
low[x]=min(low[x],dfn[y]);
}
}
模板题:洛谷T103481
强连通分量(缩点):
如果几个点可以互相到达,称为强连通分量。可以想象到这个东西像一个环一样,所以在有些问题中可以将他们缩成一个点方便去求解。
求强连通分量需要再多一个栈,去存dfs的顺序,遇到
d
f
n
[
x
]
=
=
l
o
w
[
x
]
dfn[x]==low[x]
dfn[x]==low[x]的时候,表示这个结点无法回到某一个祖先,从栈顶到这个结点构成了一个环。
关于缩点:
很多博客好像都是讲了怎么找到强连通分量,默认我这种蒟蒻会缩点了。下面是我自己的一点个人理解(方法)。
现在我们都找到了强连通分量,但是怎么判断边
(
u
,
v
)
(u,v)
(u,v)中的两点属于同一个强连通分量呢?如果不属于,该怎么样去重新建边呢?
如果暴力枚举每个点对判断的话,那么时间复杂度是
O
(
N
M
)
O(NM)
O(NM),这样的时间复杂度太高了。
我们可以想到,缩点就是把点放到同一个集合之中 。这种集合是不是类似于并查集?那么我们就可以用一个
f
a
fa
fa数组去存每个点的“父亲”点,那么我们就可以选择
l
o
w
[
x
]
=
=
d
f
n
[
x
]
low[x]==dfn[x]
low[x]==dfn[x]时候的
x
x
x作为“父亲”点。所有关于权值的操作也是对于“父亲”点的操作。那么查询和重新建边的时候,也是将两个结点的“父亲”点相连。判断这个点是不是缩点之后的点就可以用fa[x]==x
来进行判断了。这个时间复杂度只有
O
(
M
)
O(M)
O(M)。
关于如何缩点重新建图及对于点的权值操作的代码可以看一下洛谷P3387
添加最少几条边将有向图变成强连通分量: 缩点后,
max
(
c
n
t
i
n
=
0
,
c
n
t
o
u
t
=
0
)
\max(cnt_{in=0},cnt_{out=0})
max(cntin=0,cntout=0)
代码:
void tarjan(int x)
{
int i;
dfn[x]=low[x]=++num;
s.push(x);
vis[x]=1;
for(i=head[x];i;i=e[i].u)
{
int y=e[i].v;
if(!dfn[y])
{
tarjan(y);
low[x]=min(low[y],low[x]);
}
else if(vis[y])
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x])
{
sum++;
int pre;
do
{
pre=s.top();
s.pop();
vis[pre]=0;
ans[sum].pb(pre);
}while(pre!=x);
}
return;
}
模板题:洛谷P2863
双连通分量:
边双连通分量:
边双连通分量指一个无向图中,不存在割边(去掉一条边后仍互相连通)的极大子图。
先求出割边,与强连通分量类似。
添加最少几条边将无向图变成双连通分量:
⌊
l
e
a
f
+
1
2
⌋
\lfloor\frac{leaf+1}{2}\rfloor
⌊2leaf+1⌋,
l
e
a
f
leaf
leaf为叶子(度数=1)结点数量。
代码:
void tarjan(int x,int fa)
{
dfn[x]=low[x]=++num;
int i;
s.push(x);
for(i=head[x];i;i=e[i].u)
{
if(i==(fa^1))
continue;
int y=e[i].v;
if(!dfn[y])
{
tarjan(y,i);
low[x]=min(low[x],low[y]);
if(dfn[x]<low[y])
f[i]=f[i^1]=1;
}
else
low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x])
{
dcc++;
int pre;
do
{
pre=s.top();
s.pop();
c[pre]=dcc;
}while(pre!=x);
}
return;
}
模板题:洛谷P2860
点双连通分量:
等价于无向连通图不含割点。
1、孤立点是点双连通分量。
2、只要
d
f
n
[
f
a
t
h
e
r
]
≤
l
o
w
[
s
o
n
]
dfn[father]\le low[son]
dfn[father]≤low[son],就加入点双连通分量。
代码:
void tarjan(int x,int fa)
{
dfn[x]=low[x]=++num;
int i;
s.push(x);
if(head[x]==0)
{
ans[++dcc].pb(x);
return;
}
for(i=head[x];i;i=e[i].u)
{
if(i==(fa^1))
continue;
int y=e[i].v;
if(!dfn[y])
{
tarjan(y,i);
low[x]=min(low[x],low[y]);
if (low[y]>=dfn[x])
{
dcc++;
int pre;
do
{
pre=s.top();
s.pop();
ans[dcc].pb(pre);
} while (pre!=y);
ans[dcc].pb(x);
}
}
else
low[x]=min(low[x],dfn[y]);
}
return;
}
模板题:洛谷T103492