无向图的割点与点双连通分量
割点:对于一个无向图,如果删除某个节点后,连通块个数增加了,则该点为割点(也称割顶)。
割点的判定:
- 对于 DFS 树中的非根节点 \(u\),当至少存在一个子节点 \(v\),满足 \(low_v \ge dfn_u\) 时,\(u\) 为割点。
- 对于 DFS 树的根节点 \(u\),当其存在至少两个子节点满足上述条件,则 \(u\) 为割点。
为什么根节点和非根节点条件不同:因为非根节点的“上面”有一个块,所以如果 \(low_v \ge dfn_u\) 说明 \(v\) 只能通过 \(u\) 与“上面”那块连通;而根节点没有“上面”,所以需要至少两个这样的 \(v\)。
void tarjan(int u, int root) {
dfn[u] = low[u] = ++tot;
int ch = 0;
for (int v : g[u]) {
if (!dfn[v]) { // u-v为树边
tarjan(v, root);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
ch++;
if (u != root || ch > 1) {
cut[u] = true;
}
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
习题:P3388 【模板】割点(割顶)
参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::vector;
using std::min;
const int N = 20005;
vector<int> g[N];
bool cut[N];
int dfn[N], low[N], tot;
void tarjan(int u, int root) {
dfn[u] = low[u] = ++tot;
int ch = 0;
for (int v : g[u]) {
if (!dfn[v]) { // u-v为树边
tarjan(v, root);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
ch++;
if (u != root || ch > 1) {
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 x, y; scanf("%d%d", &x, &y);
g[x].push_back(y); g[y].push_back(x);
}
for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i, i);
int ans = 0;
for (int i = 1; i <= n; i++) if (cut[i]) ans++;
printf("%d\n", ans);
for (int i = 1; i <= n; i++) if (cut[i]) printf("%d ", i);
return 0;
}
若一张无向图不存在割点,则称其为点双连通图。无向图的极大点双连通子图称为点双连通分量(vertex Double Connected Components, vDCC)。

void tarjan(int u, int root) {
dfn[u] = low[u] = ++tot;
int ch = 0;
s.push(u);
for (int v : g[u]) {
if (!dfn[v]) {
tarjan(v, root);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
ch++; cnt++;
while (s.top() != v) {
ans[cnt].push_back(s.top()); s.pop();
}
ans[cnt].push_back(v); s.pop();
ans[cnt].push_back(u); // 栈顶到v的部分和u共同组成一个点双连通分量
}
} else if (ins[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (u == root && ch == 0) { // 孤立点要特判
cnt++; ans[cnt].push_back(u);
}
}
习题:P8435 【模板】点双连通分量
参考代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <stack>
using namespace std;
const int N = 500005;
vector<int> g[N], ans[N];
stack<int> s;
int low[N], dfn[N], tot, cnt;
void tarjan(int u, int root) {
dfn[u] = low[u] = ++tot;
int ch = 0;
s.push(u);
for (int v : g[u]) {
if (!dfn[v]) {
tarjan(v, root);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
ch++; cnt++;
while (s.top() != v) {
ans[cnt].push_back(s.top()); s.pop();
}
ans[cnt].push_back(v); s.pop();
ans[cnt].push_back(u);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
if (u == root && ch == 0) {
cnt++; ans[cnt].push_back(u);
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y; scanf("%d%d", &x, &y);
g[x].push_back(y); g[y].push_back(x);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i, i);
printf("%d\n", cnt);
for (int i = 1; i <= cnt; i++) {
printf("%d", (int)ans[i].size());
for (int j : ans[i]) printf(" %d", j);
printf("\n");
}
return 0;
}
习题:P5058 [ZJOI2004] 嗅探器
解题思路
本题相当于求能将 \(a\) 和 \(b\) 隔开的割点。
回想一下找割点的算法,如何判断 \(a\) 和 \(b\) 刚好分布在找到的割点两侧?
考虑以 \(a\) 为搜索树的根节点,这样一来当根据割点条件发现某个割点 \(u\) 时,\(dfn_a\) 必然小于 \(dfn_u\),此时如果 \(b\) 在以 \(u\) 为根的 DFS 子树内,则 \(a\) 和 \(b\) 就分布在割点两侧。因此在割点的基础条件上增加 \(dfn_b \ge dfn_u\) 的判断即可。
参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::vector;
using std::min;
const int N = 200005;
vector<int> g[N];
int a, b, dfn[N], low[N], tot;
bool cut[N];
void tarjan(int u) {
dfn[u] = low[u] = ++tot;
for (int v : g[u]) {
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u] && u != a && dfn[b] >= dfn[v]) {
cut[u] = true;
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
int n;
scanf("%d", &n);
while (true) {
int i, j;
scanf("%d%d", &i, &j);
if (i == 0 && j == 0) break;
g[i].push_back(j); g[j].push_back(i);
}
scanf("%d%d", &a, &b);
tarjan(a);
for (int i = 1; i <= n; i++)
if (cut[i]) {
printf("%d\n", i);
return 0;
}
printf("No solution\n");
return 0;
}
习题:P3225 [HNOI2012] 矿场搭建
解题思路
整张图可以拆分成若干个点双连通分量的组合,对于分解出来的每个 vDCC 来说,如果其中没有割点,说明该连通块整个构成一个点双连通分量,此时需要在其中放置两个救援出口(这样当某个点坍塌后剩下点都能通向另一个出口),设大小为 \(s\),则其中任选两个点作为出口的方案数为 \(\dfrac{s(s-1)}{2}\);如果其中有一个割点,说明该分量通过那个割点与图中其他部分连接,如果有不止一个割点,则说明跟多个其他的点双连通分量连接,而一个连通块中如果由多个 vDCC 组成,则只需要在每个割点数为 \(1\) 的 vDCC 中分别选一个非割点的位置放置一个出口即可(这样一来当坍塌的是某个 vDCC 中的非割点时,该 vDCC 中其他点可以沿着割点去到其他 vDCC 中的某个出口,如果坍塌的刚好是割点,那么该割点涉及到的每个 vDCC 中都预先准备过出口),每个 vDCC 中可以选择的方案数是 \(s-1\),每个 vDCC 之间独立,方案数累乘。
参考代码
#include <cstdio>
#include <vector>
#include <stack>
#include <algorithm>
using std::vector;
using std::stack;
using std::min;
using ull = unsigned long long;
const int N = 1005;
vector<int> g[N], vDCC[N];
int dfn[N], low[N], tot, ans1, cnt;
bool cut[N];
ull ans2;
stack<int> s;
void tarjan(int u, int root) {
dfn[u] = low[u] = ++tot;
s.push(u);
int ch = 0;
for (int v : g[u]) {
if (!dfn[v]) {
tarjan(v, root);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
ch++;
if (ch > 1 || u != root) { // u是割点
cut[u] = true;
}
cnt++;
while (true) {
int t = s.top(); s.pop();
vDCC[cnt].push_back(t);
if (t == v) break;
}
vDCC[cnt].push_back(u);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
int main()
{
int n, case_id = 0;
while (true) {
scanf("%d", &n);
if (n == 0) break;
case_id++;
tot = 0; ans1 = 0; ans2 = 1; cnt = 0;
for (int i = 1; i <= 1000; i++) {
g[i].clear(); vDCC[i].clear();
dfn[i] = low[i] = 0;
cut[i] = false;
}
for (int i = 1; i <= n; i++) {
int x, y; scanf("%d%d", &x, &y);
g[x].push_back(y); g[y].push_back(x);
}
while (!s.empty()) s.pop();
for (int i = 1; i <= 1000; i++)
if (!dfn[i]) tarjan(i, i);
for (int i = 1; i <= cnt; i++) {
int cut_cnt = 0;
for (int j : vDCC[i]) {
if (cut[j]) cut_cnt++;
}
ull sz = vDCC[i].size();
if (cut_cnt == 0) {
ans1 += 2; ans2 *= (sz - 1) * sz / 2;
} else if (cut_cnt == 1) {
ans1++; ans2 *= (ull)(sz - 1);
}
}
printf("Case %d: %d %llu\n", case_id, ans1, ans2);
}
}

浙公网安备 33010602011771号