定义
割点:给定一无向连通图,对于其中一点 u u u,若从图中删掉 u u u 和所有与 u u u 相连的边后,原图分裂成成 2 2 2 个或以上不相连的子图,则称 u u u 为原图的割点(或割顶)。
割边:给定一无向连通图,对于其中一边 ( u , v ) (u,v) (u,v),若从图中删掉 ( u , v ) (u,v) (u,v) 后,原图分裂成 2 2 2 个或以上不相连的子图,则称 ( u , v ) (u,v) (u,v) 为原图的割边(或桥)。
T a r j a n Tarjan Tarjan 算法可以在 O ( n ) O(n) O(n) 内求出所有割点与割边。
割点
对于无向图的每一个联通块,我们可以把他看成一棵树,即把我们假定的根节点拎起来。
先思考,怎样才能判断出从图中删掉 u u u 和所有与 u u u 相连的边后,原图会不联通。
根据之前求 强连通分量 时对 d f n dfn dfn1 和 l o w low low2 的定义。
易得,当 d f n ( u ) ≤ l o w ( v ) dfn(u)\le low(v) dfn(u)≤low(v) 时,说明从 v v v 和 v v v 的子树 出发,若不经过 u u u,则无法到达比 u u u 的 d f n dfn dfn 更小的节点,那么我们把 u u u 删掉,原图就被分成了 v v v 和 v v v 的子树 和 剩下的节点 至少 2 2 2 个子图。
若 u u u 是割点,则 u u u 满足一下任一条件:
-
u u u 不是搜索树的根节点且 u u u 至少有 1 1 1 个子节点满足: d f n ( u ) ≤ l o w ( v ) dfn(u)\le low(v) dfn(u)≤low(v);
-
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)3。
这时可能有人要问了,如果图是这样的呢? 1 1 1 还是割点吗?根据上面的判法不会错吗?
对于这种图,我可以明确的告诉你们,无向图是没有横向边的,把他想成这样就行了。
CODE
#include <bits/stdc++.h>
using namespace std;
struct Fastio
{
template <typename T>
inline Fastio operator>>(T &x)
{
x = 0;
char c = getchar();
while (c < '0' || c > '9')
c = getchar();
while (c >= '0' && c <= '9')
x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
return *this;
}
inline Fastio &operator<<(const char *str)
{
int cur = 0;
while (str[cur])putchar(str[cur++]);
return *this;
}
template <typename T>
inline Fastio &operator<<(T x)
{
if (x == 0)
{
putchar('0');
return *this;
}
if (x < 0) putchar('-'), x = -x;
static int sta[45];
int top = 0;
while (x) sta[++top] = x % 10, x /= 10;
while (top) putchar(sta[top] + '0'), --top;
return *this;
}
} io;
int n, m, rt, ans, cnt_node, cntn;
int cnt;
array<int, 2000005> head;
struct abc
{
int to, nxt;
};
array<abc, 2000005> dd;
array<bool, 2000005> cut;
array<int, 2000005> dfn, low;
inline void add(int u, int v)
{
dd[++cnt].to = v;
dd[cnt].nxt = head[u];
head[u] = cnt;
}
inline void tarjan(int u)
{
dfn[u] = low[u] = ++cnt_node;
int flag = 0;
for (int e = head[u]; e; e = dd[e].nxt)
{
int v = dd[e].to;
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[v], low[u]);
if(dfn[u] <= low[v])
{
flag++;
if((u != rt || flag > 1) && !cut[u])
{
cut[u] = 1;
cntn++;
}
}
}
else low[u] = min(low[u], dfn[v]);
}
}
signed main()
{
io >> n >> m;
for(int i = 1; i <= m; ++i)
{
int u, v;
io >> u >> v;
add(u, v);
add(v, u);
}
for(int i = 1; i <= n; ++i)
if(!dfn[i]) tarjan(rt = i);
cout << cntn << endl;
for(int i = 1; i <= n; ++i)
if(cut[i]) io << i << " ";
return 0;
}
割边
与割点类似,易得,当 d f n ( u ) < l o w ( v ) dfn(u) < low(v) dfn(u)<low(v) 时,说明若 v v v 和 v v v 的子树不存在一条连接 u u u 或其祖先的后向边,那么我们把 ( u , v ) (u,v) (u,v) 删掉,原图就被分成了 v v v 和 v v v 的子树 和 剩下的节点 至少 2 2 2 个子图。
定理: ( u , v (u,v (u,v) 为桥当且仅当它不在任何一个简单回路中。
若 ( u , v ) (u,v) (u,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 s dfs dfs 一直传递下去。
由于一条无向边是由两条有向边组成的且这两条有向边的编号是相差 1 1 1,所以我们通过一条边 x x x 的编号,得知另一条边 y y y 的编号。
判断方法:
-
若 x % 2 = 0 x \ \% \ 2=0 x % 2=0,则 y = x − 1 y=x-1 y=x−1。
-
若 x % 2 = 1 x \ \% \ 2=1 x % 2=1,则 y = x + 1 y=x+1 y=x+1。
CODE
#include <bits/stdc++.h>
using namespace std;
struct Fastio
{
template <typename T>
inline Fastio operator>>(T &x)
{
x = 0;
char c = getchar();
while (c < '0' || c > '9')
c = getchar();
while (c >= '0' && c <= '9')
x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
return *this;
}
inline Fastio &operator<<(const char *str)
{
int cur = 0;
while (str[cur])putchar(str[cur++]);
return *this;
}
template <typename T>
inline Fastio &operator<<(T x)
{
if (x == 0)
{
putchar('0');
return *this;
}
if (x < 0) putchar('-'), x = -x;
static int sta[45];
int top = 0;
while (x) sta[++top] = x % 10, x /= 10;
while (top) putchar(sta[top] + '0'), --top;
return *this;
}
} io;
int n, m, cnt_node, cntn;
struct abc
{
int a, b;
} ans[2000005];
bool cmp(abc a, abc b)
{
if(a.a != b.a) return a.a < b.a;
return a.b < b.b;
}
int cnt;
array<int, 2000005> head;
struct abcd
{
int to, nxt;
};
array<abcd, 2000005> dd;
array<int, 2000005> dfn, low;
inline int js(int x)
{
return x % 2 ? x + 1 : x - 1;
}
inline void add(int u, int v)
{
dd[++cnt].to = v;
dd[cnt].nxt = head[u];
head[u] = cnt;
}
inline void tarjan(int u, int edge)
{
dfn[u] = low[u] = ++cnt_node;
for (int e = head[u]; e; e = dd[e].nxt)
{
int v = dd[e].to;
if (!dfn[v])
{
tarjan(v, e);
low[u] = min(low[v], low[u]);
if(low[v] > dfn[u]) ans[++cntn] = (abc){min(u, v), max(v, u)};
}
else if (e != js(edge))
{
low[u] = min(low[u], dfn[v]);
}
}
}
signed main()
{
io >> n >> m;
for(int i = 1; i <= m; ++i)
{
int u, v;
io >> u >> v;
add(u, v);
add(v, u);
}
for(int i = 1; i <= n; ++i)
if(!dfn[i]) tarjan(i, 0);
sort(ans + 1, ans + cntn + 1, cmp);
for(int i = 1; i <= cntn; ++i) io << ans[i].a << " " << ans[i].b << "\n";
return 0;
}
d f s n [ u ] dfsn[u] dfsn[u]:深度优先搜索遍历时结点 u u u 被搜索的次序(也称为 d f s dfs dfs 序)。 ↩︎
l o w [ u ] low[u] low[u]:定义为 u u u 所在子树的节点经过最多一条非树边 u → v u \to v u→v (其中 v v v 必须可达 u u u )能到达的节点中最小的 d f s dfs dfs 序。 ↩︎
如下图:
当 u u u 为搜索树的根节点时, 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) 时, u u u 才是割点。 ↩︎