割点与桥
基本概念
-
割点:在无向图中,如果删去某个点\(v\)会使得图中的极大连通分量个数增加,则称点\(v\)为割点。
-
桥(割边):在无向图中,如果删去某条无向边\((u, v)\)会使得图中的极大连通分量个数增加,则称边\((u, v)\)是割边。
算法思想
割点
若一个顶点\(u\)是割点,当且仅当它满足以下条件之一:
-
\(u\)不是根结点,且其存在一个子结点\(v\),使得\(low_{v}\) \(\geq dfn_{u}\)
,说明\(v\)无法不通过\(u\)到达其祖先。证明:如果\(v\)无法不通过\(u\)到达其祖先,说明\(v\)仅与\(u\)和\(u\)的子树连边,割掉\(u\)后\(v\)一定会形成一个新的强连通分量。 -
\(u\)是根结点,且\(u\)有两个或以上的子树。证明:如果割掉\(u\)后,\(u\)的多个子树会无法连通,即形成新的强连通分量。
如果某个顶点\(u\)有一个子结点\(v\),其\(low\)值小于\(u\)的时间戳,说明\(v\)可以不经过\(u\)到达其祖先,所以\(u\)不是割点。
值得注意的是,割点算法的tarjan
函数并不能正确地求出图中连通分量的个数、大小和其包含的顶点。因此,在有需要的情况下(如这道题),我们可以写出两个tarjan
函数用于不同的用途,或是用特殊的方法更新连通分量的个数,具体见笔者的这篇博文。
注意到删除图中的一个割点\(u\),对于每一个\(u\)的子结点\(v\),如果\(low_{v} \geq dfn_{u}\),说明\(v\)只与\(u\)和\(u\)的子树连边,所以删去\(u\)后,\(v\)无法与\(u\)的祖先连通,即\(v\)会形成新的强连通分量。
因此,我们可以猜想:如果点\(v\)与点\(u\)是父子关系,且\(low_{v} \geq dfn_{u}\),删去\(u\)后增加的连通分量个数是否等于满足条件的\(v\)的个数?
很显然,如果\(u\)是其所在搜索树的根结点,且其子树个数为\(n\),删去\(u\)后增加的连通分量个数应该是\(n - 1\)。因为\(u\)的子树原本同属于一个连通分量,割去\(u\),相当于把\(n - 1\)棵子树剥离出原来的树\(T\),再把\(T\)的根结点\(u\)删除。因此,增加的连通分量个数应该是剥离出的子树个数,即\(n - 1\)。
对于一个点\(u\),其\(cut\)值有以下两种情况:
-
\(u\)是其所在搜索树的根结点,若\(u\)的子树个数为\(n\),则\(cut_{u} = n - 1\)。
-
\(u\)不是其所在搜索树的根结点,若\(u\)的子树个数为\(n\),则\(cut_{u} = n\)。
割边
如果有一对父子顶点\((u, v)\),使得\(low_{v}\) \(>\) \(dfn_{u}\),则边\((u, v)\)是割边。
证明:回顾\(low_{u}\)的定义——不经过\(u\)的父结点所能达到的最小的时间戳。我们将\((u, v)\)删除,相当于求解\(low_{v}\)的值。如果\(low_{v} > dfn_{u}\),说明\(v\)无法到达\(u\)和\(u\)的祖先,即删除\((u, v)\)后,\(v\)会形成新的强连通分量。
值得注意的是,如果图\(G\)中存在重边,tarjan
函数是不能记录上一个结点的编号以避免往回走的。我们应该记录上一条边的编号,设当前边编号为i
,上一条边编号为from
,如果from ^ 1 == i
,说明边i
不可以走,要continue
。使用这样的方法判断有一个前提:第一条边的编号从0
或2
开始。
模板
割点
#include <cstdio>
#include <stack>
#include <algorithm>
using namespace std;
const int maxn = 2 * 1e4 + 5;
const int maxm = 2e5 + 5;
struct node {
int to, nxt;
}edge[maxm];
int n, m, cntn;
int cnt_node, cnt_edge, cnt_scc;
int head[maxn], low[maxn], dfn[maxn], color[maxn];
bool in_stack[maxn], cut[maxn];
stack<int> s;
inline void add_edge(int u, int v) {
cnt_edge++;
edge[cnt_edge].to = v;
edge[cnt_edge].nxt = head[u];
head[u] = cnt_edge;
}
void tarjan(int u, int fa) {
int child = 0;
cnt_node++;
dfn[u] = cnt_node;
low[u] = cnt_node;
s.push(u);
in_stack[u] = true;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if (!dfn[v]) {
child++; //发现u的新子结点
tarjan(v, u);
low[u] = min(low[u], low[v]);
//如果u点满足割点条件且不是根结点
if (low[v] >= dfn[u] && u != fa && !cut[u]) {
cut[u] = true;
cntn++;
}
} else if (in_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
//判断根结点是否是割点
if (child >= 2 && u == fa && !cut[u]) {
cut[u] = true;
cntn++;
}
}
int main() {
int u, v;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &u, &v);
add_edge(u, v);
add_edge(v, u);
}
for (int i = 1; i <= n; i++) {
if (!color[i]) {
tarjan(i, i); //发现新的搜索树(连通分量)
}
}
printf("%d\n", cntn);
for (int i = 1; i <= n; i++) {
if (cut[i]) {
printf("%d ", i);
}
}
puts("");
return 0;
}
割边
参考代码如下:
#include <cstdio>
#include <cstring>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 5;
const int maxm = 5e5 + 5;
int n, m;
int cnt_node, cnt_scc;
int low[maxn], dfn[maxn], color[maxn];
bool in_stack[maxn];
vector<int> g[maxn];
vector<pair<int, int> > bridge;
stack<int> s;
void init() {
memset(low, 0, (n + 1) * sizeof(int));
memset(dfn, 0, (n + 1) * sizeof(int));
memset(color, 0, (n + 1) * sizeof(int));
memset(in_stack, false, (n + 1) * sizeof(bool));
for (int i = 1; i <= n; i++) {
while (!g[i].empty()) {
g[i].pop_back();
}
}
while (!bridge.empty()) {
bridge.pop_back();
}
cnt_node = cnt_scc = 0;
}
void tarjan(int u, int fa) {
cnt_node++;
dfn[u] = cnt_node;
low[u] = cnt_node;
s.push(u);
in_stack[u] = true;
for (int i = 0; i < g[u].size(); i++) {
int v = g[u][i];
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
int f = min(u, v);
int s = max(u, v);
bridge.push_back(make_pair(f, s));
}
} else if (in_stack[v] && v != fa) {
low[u] = min(low[u], dfn[v]);
}
}
}
int main() {
int u, v;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
if (!color[i]) {
tarjan(i, 0);
}
}
sort(bridge.begin(), bridge.end());
for (int i = 0; i < bridge.size(); i++) {
printf("%d %d\n", bridge[i].first, bridge[i].second);
}
return 0;
}
例题:\(hdu4738\)(有重边)
参考代码如下:
#include <cstdio>
#include <cstring>
#include <stack>
#include <algorithm>
using namespace std;
const int maxn = 1e3 + 5;
const int maxm = 1e6 + 5;
const int inf = 0x3f3f3f3f;
struct node
{
int to, nxt, w;
}edge[maxm];
int n, m, ans;
int cnt_ver, cnt_edge, cnt_scc;
int head[maxn], dfn[maxn], low[maxn];
bool in_stack[maxn], bridge[maxn];
stack<int> s;
void init()
{
memset(head, 0, sizeof(head));
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(in_stack, false, sizeof(in_stack));
memset(bridge, false, sizeof(bridge));
while (!s.empty())
s.pop();
ans = inf;
cnt_ver = cnt_scc = 0;
cnt_edge = 1;
}
void add_edge(int u, int v, int w)
{
cnt_edge++;
edge[cnt_edge].to = v;
edge[cnt_edge].w = w;
edge[cnt_edge].nxt = head[u];
head[u] = cnt_edge;
}
void tarjan(int u, int from)
{
cnt_ver++;
dfn[u] = cnt_ver;
low[u] = cnt_ver;
s.push(u);
in_stack[u] = true;
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
{
bridge[i] = bridge[i ^ 1] = true;
ans = min(ans, edge[i].w);
}
}
else if (i != (from ^ 1))
low[u] = min(low[u], dfn[v]);
}
}
int main()
{
int u, v, w;
while (scanf("%d%d", &n, &m) != EOF)
{
if (!n && !m)
break;
init();
for (int i = 1; i <= m; i++)
{
scanf("%d%d%d", &u, &v, &w);
add_edge(u, v, w);
add_edge(v, u, w);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
cnt_scc++;
tarjan(i, -1);
}
}
if (cnt_scc > 1)
printf("%d\n", 0);
else if (!ans)
printf("%d\n", 1);
else if (ans == inf)
printf("%d\n", -1);
else
printf("%d\n", ans);
}
return 0;
}