无向图的割点与桥
给定无向连通图:
- 对于其中一点 \(u\),若从图中删掉 \(u\) 和所有与 \(u\) 相连的边后,原图分裂成成 \(2\) 个或以上不相连的子图,则称 \(u\) 为原图的割点(或割顶)。
- 对于其中一边 \(e\),若从图中删掉 \(e\) 后,原图分裂成 \(2\) 个或以上不相连的子图,则称 \(e\) 为原图的桥(或割边)。
- 一般无向图(不保证连通)的割点与桥就是它各个连通块的割点与桥。
用 \(\rm Tarjan\) 算法可以在 \(\operatorname{O}(n)\) 内求出所有割点与桥。
跟求 \(\rm SCC\) 类似,我们也需要用到 \(dfn\) 和 \(low\) 数组,其意义和求 \(\rm SCC\) 时的 \(dfn,low\) 数组类似。
1. 割点
若 \(u\) 不是搜索树的 \(root\),则 \(u\) 是割点当且仅当树上至少有 \(u\) 的 \(1\) 个子节点 \(v\) 满足:
若 \(u\) 是 \(root\),则 \(u\) 是割点当且仅当 \(u\) 至少有 \(2\) 个子节点满足上述条件。
\(dfn(u)\le low(v)\) 说明从 \(subtree(v)\) 出发,若不经过 \(u\),则无法到达比 \(u\) 的 \(dfn\) 更小的节点,那么我们把 \(u\) 删掉,原图就被分成了 \(subtree(v)\) 和剩下的节点至少 \(2\) 个子图。由于割点允许 \(dfn(u)=low(v)\),所以 \(dfn(fa)\) 可以用来更新 \(low(u)\)。。
#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\) 的子节点是 \(v\),则边 \(<u,v>\) 是桥,当且仅当:
\(dfn(u)<low(v)\) 说明从 \(subtree(v)\) 出发,若不经过 \(<u,v>\),则无法到达 比 \(u\) 的 \(dfn\) 更小的节点,那么我们把 \(<u,v>\) 删掉,原图就被分成了 \(subtree(v)\) 和剩下的节点至少 \(2\) 个子图。
值得注意的是:因为是无向边,所以从 \(u\) 出发总能回到它的 \(fa\)。根据 \(low\) 的定义,\(<u,fa>\) 是树边且 \(fa\notin subtree(u)\),所以 \(dfn(fa)\) 不能用来更新 \(low(u)\)!!!
但是你以为这样就完了吗?
毒瘤数据会出现重边!!!
对于重边,只有一条算树边,所以有重边时,\(dfn(fa)\) 又能用来更新 \(low(u)\) 了。
机房某 dalao:你\(*\)炸了
处理方法:将读入的边成对储存在 \(e(2)\) 和 \(e(3)\),\(e(4)\) 和
\(e(5)\dots e(2n)\) 和 \(e(2n+1)\) 里。
观察:
\(2\,\operatorname{xor}\,1=3\)
\(4\,\operatorname{xor}\,1=5\)
\(\cdots\cdots\)
\(2n\,\operatorname{xor}\,1=2n+1\)
若通过 \(e(i)\) 进入 \(u\),则 \(e(i)\) 和 \(e(i\operatorname{xor}1)\) 本质上是同一条无向边,故除了 \(e(i\operatorname{xor}1)\) 之外的边都能用来更新 \(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;
}
一个好玩的性质
除了图中只有两点一线的情况外,桥的两个端点一定都是割点、