给定无向连通图:
- 对于其中一点 u u u,若从图中删掉 u u u 和所有与 u u u 相连的边后,原图分裂成成 2 2 2 个或以上不相连的子图,则称 u u u 为原图的割点(或割顶)。
- 对于其中一边 e e e,若从图中删掉 e e e 后,原图分裂成 2 2 2 个或以上不相连的子图,则称 e e e 为原图的桥(或割边)。
- 一般无向图(不保证连通)的割点与桥就是它各个连通块的割点与桥。
用 T a r j a n \rm Tarjan Tarjan 算法可以在 O ( n ) \operatorname{O}(n) O(n) 内求出所有割点与桥。
跟求 S C C \rm SCC SCC 类似,我们也需要用到 d f n dfn dfn 和 l o w low low 数组,其意义和求 S C C \rm SCC SCC 时的 d f n , l o w dfn,low dfn,low 数组类似。
1. 割点
若
u
u
u 不是搜索树的
r
o
o
t
root
root,则
u
u
u 是割点当且仅当树上至少有
u
u
u 的
1
1
1 个子节点
v
v
v 满足:
d
f
n
(
u
)
≤
l
o
w
(
v
)
dfn(u)\le low(v)
dfn(u)≤low(v)
若
u
u
u 是
r
o
o
t
root
root,则
u
u
u 是割点当且仅当
u
u
u 至少有
2
2
2 个子节点满足上述条件。
d f n ( u ) ≤ l o w ( v ) dfn(u)\le low(v) dfn(u)≤low(v) 说明从 s u b t r e e ( v ) subtree(v) subtree(v) 出发,若不经过 u u u,则无法到达比 u u u 的 d f n dfn dfn 更小的节点,那么我们把 u u u 删掉,原图就被分成了 s u b t r e e ( v ) subtree(v) subtree(v) 和剩下的节点至少 2 2 2 个子图。
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2e4 + 5;
const int MAXM = 1e5 + 5;
int cnt, Time, rt, tot;
int head[MAXN], dfn[MAXN], low[MAXN];
bool cut[MAXN];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++Time; //初始化dfn和low
int flag = 0;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v]) //low值的更新和求SCC时类似
{
tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v])
{
flag++;
if (u != rt || flag > 1) //满足x不是根节点,或者x是根节点且有至少2个满足要求的子节点
{
if (!cut[u]) //防止重复统计
{
tot++;
}
cut[u] = true;
}
}
}
else
{
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(rt = i); //搜索树的根是i
}
}
printf("%d\n", tot);
for (int i = 1; i <= n; i++)
{
if (cut[i])
{
printf("%d ", i);
}
}
return 0;
}
2. 桥
搜索树上
u
u
u 的子节点是
v
v
v,则边
<
u
,
v
>
<u,v>
<u,v> 是桥,当且仅当:
d
f
n
(
u
)
<
l
o
w
(
v
)
dfn(u)<low(v)
dfn(u)<low(v)
d f n ( u ) < l o w ( v ) dfn(u)<low(v) dfn(u)<low(v) 说明从 s u b t r e e ( v ) subtree(v) subtree(v) 出发,若不经过 < u , v > <u,v> <u,v>,则无法到达 比 u u u 的 d f n dfn dfn 更小的节点,那么我们把 < u , v > <u,v> <u,v> 删掉,原图就被分成了 s u b t r e e ( v ) subtree(v) subtree(v) 和剩下的节点至少 2 2 2 个子图。
值得注意的是:因为是无向边,所以从 u u u 出发总能回到它的 f a fa fa。根据 l o w low low 的定义, < u , f a > <u,fa> <u,fa> 是树边且 f a ∉ s u b t r e e ( u ) fa\notin subtree(u) fa∈/subtree(u),所以 d f n ( f a ) dfn(fa) dfn(fa) 不能用来更新 l o w ( u ) low(u) low(u)!!!
但是你以为这样就完了吗?
毒瘤数据会出现重边!!!
对于重边,只有一条算树边,所以有重边时, d f n ( f a ) dfn(fa) dfn(fa) 又能用来更新 l o w ( u ) low(u) low(u) 了。
机房某 dalao:你
∗
*
∗炸了
处理方法:将读入的边成对储存在
e
(
2
)
e(2)
e(2) 和
e
(
3
)
e(3)
e(3),
e
(
4
)
e(4)
e(4) 和
e
(
5
)
…
e
(
2
n
)
e(5)\dots e(2n)
e(5)…e(2n) 和
e
(
2
n
+
1
)
e(2n+1)
e(2n+1) 里。
观察:
2 xor 1 = 3 2\,\operatorname{xor}\,1=3 2xor1=3
4 xor 1 = 5 4\,\operatorname{xor}\,1=5 4xor1=5
⋯ ⋯ \cdots\cdots ⋯⋯
2 n xor 1 = 2 n + 1 2n\,\operatorname{xor}\,1=2n+1 2nxor1=2n+1
若通过 e ( i ) e(i) e(i) 进入 u u u,则 e ( i ) e(i) e(i) 和 e ( i xor 1 ) e(i\operatorname{xor}1) e(ixor1) 本质上是同一条无向边,故除了 e ( i xor 1 ) e(i\operatorname{xor}1) e(ixor1) 之外的边都能用来更新 l o w ( u ) low(u) low(u)。
另外吐槽一句,这题作为桥的模板本来应该评绿的,结果因为数据范围较小可以用暴力过直接给评黄了……
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 155;
const int MAXM = 5005;
int cnt = 1, Time, tot; //注意这里!因为边是存在(2,3),(4,5)……内的,所以cnt要初始化为1!
int head[MAXN], dfn[MAXN], low[MAXN];
struct edge
{
int to, nxt;
}e[MAXM << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
struct ans
{
int from, to;
bool operator <(const ans &x)const
{
if (x.from != from)
{
return x.from > from;
}
return x.to > to;
}
}a[MAXM << 1];
void add_ans(int u, int v)
{
a[++tot] = ans{min(u, v), max(u, v)};
}
void tarjan(int u, int in_edge)
{
dfn[u] = low[u] = ++Time;
for (int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v]) //是桥,把答案存进去
{
add_ans(u, v);
}
}
else if (i != (in_edge ^ 1)) //不是同一条无向边
{
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(i, 0);
}
}
sort(a + 1, a + tot + 1); //按照题目要求输出
for (int i = 1; i <= tot; i++)
{
printf("%d %d\n", a[i].from, a[i].to);
}
return 0;
}
一个好玩的性质
除两点一线的情况外,桥的两个端点一定都是割点。