答案 = 原连通块数 - 1 + 删点最大裂块数:一次迭代 Tarjan 维护 dfn/low,非根点裂块数 = 1 + low ≥ dfn 的孩子数,根等于子树数,O(P+C) 求最大值。
OJ: roj
题目 ID: 1525
难度:普及+/提高-
标签:图论Tarjan割点连通块python
创建: 2026-09-30 16:17
更新: 2026-10-07 11:01
形式化题目
给定一个无向图(不保证连通、可能有孤立点,无重边)。对每个顶点
样例第一组是三角形(三点两两相连):任删一点,剩下两点之间仍有边,仍是一个连通块,输出 1。
样例第二组是四点两条边,
graph LR
subgraph comp1["连通块 A"]
a0["0"] --- a1["1"]
end
subgraph comp2["连通块 B"]
b2["2"] --- b3["3"]
end
删去
样例第三组只有边
正解
思路
朴素做法与瓶颈。 对每个顶点
第一步:把「删谁」的枚举分解成计数。 删
其中
第二步:把
- 非根
:各分离孩子自成一块,父方向与所有非分离孩子的子树仍连成一块,故 ; - DFS 树的根:没有父方向那一块,
子树数;根的孩子恒满足 (根是本连通块里最小的 ),所以同一套「分离孩子数」统计对根恰好就是子树数,只是不加 1; - 孤立点:是「根且零孩子」,
,公式自然退化,无需特判。
一个最小的割点例子:顶点
| 点 |
DFS 树中的孩子 | ||||
|---|---|---|---|---|---|
| 0(根) | 0 | 0 | 1 | 1 | 根不加 1:1 |
| 1 | 1 | 1 | 2 | 1 | |
| 2 | 2 | 2 | — | 0 |
表中 3 2(边
第三步:一次 DFS 收齐所有答案。 弹栈时把子点的
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 16:10
# update_at: 2026-09-30 16:10
import sys
def max_pieces(adj: list[list[int]]) -> tuple[int, int]:
"""迭代版 Tarjan:返回(原图连通块数,删去单点后其所在连通块最多裂成的块数)。"""
n = len(adj)
dfn = [-1] * n # DFS 进入时刻,-1 表示未访问
low = [0] * n # 子树经回边能到达的最小 dfn
split = [0] * n # u 的孩子里 low[child] >= dfn[u] 的个数
clock = 0
comps = 0 # 原图连通块数
best = 0 # 所有顶点中"删去后裂成的块数"的最大值
dfs: list[list[int]] = [] # 栈帧:[顶点, 父顶点, 下一条待查邻边位置],迭代代替递归
for root in range(n):
if dfn[root] >= 0:
continue
comps += 1
dfn[root] = low[root] = clock
clock += 1
dfs.append([root, -1, 0])
while dfs:
u, par, i = dfs[-1]
if i < len(adj[u]):
dfs[-1][2] = i + 1
v = adj[u][i]
if dfn[v] < 0: # 树边:向下展开
dfn[v] = low[v] = clock
clock += 1
dfs.append([v, u, 0])
elif v != par and dfn[v] < dfn[u]: # 回边:对称两边只取编号小的一端
low[u] = min(low[u], dfn[v])
else: # u 的邻边走完:向父回传 low 并结算
dfs.pop()
if par >= 0:
low[par] = min(low[par], low[u])
if low[u] >= dfn[par]: # u 的子树绕不开 par,par 是割点
split[par] += 1
# 非根删点后 = 各闭合子树 + 父方向那一大块;根只有各子树
best = max(best, split[u] + (par >= 0))
return comps, best
def solve() -> None:
# 逐行惰性切分:整份输入有 7+MB,一次性 read().split() 会把峰值内存推近 128MB 限制
lines = (ln.split() for ln in sys.stdin.buffer if ln.strip())
out: list[str] = []
for first in lines: # 每组第一行 "P C",以 0 0 结束
p_cnt, edge_cnt = int(first[0]), int(first[1])
if p_cnt == 0:
break
adj: list[list[int]] = [[] for _ in range(p_cnt)]
for _ in range(edge_cnt): # 保证无重边,直接双向建表
a, b = map(int, next(lines))
adj[a].append(b)
adj[b].append(a)
comps, best = max_pieces(adj)
out.append(str(comps - 1 + best)) # 删点后总数 = 其余连通块 + 其所在块的裂块数
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
solve()C++ 版(同一算法):
// main.cpp:「一本通 3.6 练习 4」电力 —— 求删去一个点后连通块最多有多少。
// 思路:答案 = 原连通块数 - 1 + max pieces(v)。
// pieces(v) 用一次迭代 Tarjan(dfn/low)统计:非根 v 的 pieces = 1 + 分离孩子数
// (孩子 c 满足 low[c] >= dfn[v],即 c 的子树绕不开 v);根的 pieces = 子树数
// (根的分离孩子数恰好就是子树数,但不再加 1);孤立点自然得 0。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 06:13
* update_at: 2026-10-05 06:13
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
typedef long long ll;
int n, m;
vector<int> g[MAXN]; // 邻接表:g[u] = u 的所有邻点
int dfn[MAXN]; // DFS 进入时刻,0 表示未访问;时刻从 1 开始编号
int low[MAXN]; // low[u] = u 的子树经回边能到达的最小 dfn
int split_cnt[MAXN]; // u 的孩子里 low[child] >= dfn[u] 的个数(分离孩子数)
int tick; // dfn 计时器
int comps; // 原图连通块数
int best; // 所有顶点中「删去后其所在块裂成的块数」的最大值
// 加一条无向边。
void add_edge(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
}
// 对 start 所在连通块做一次迭代 Tarjan(P 可达 10000,链状图会打穿递归栈)。
// 弹栈时向父回传 low、给父累加分离孩子数,并用当前点更新 best。
void dfs(int start) {
// 栈帧:[顶点 u, 父顶点 par, 下一条待查邻边下标 i, 邻边总数 sz]
static int st_u[MAXN], st_par[MAXN], st_i[MAXN], st_sz[MAXN];
int top = 0;
tick++;
dfn[start] = low[start] = tick;
top++;
st_u[top] = start;
st_par[top] = -1;
st_i[top] = 0;
st_sz[top] = g[start].size();
while (top > 0) {
int u = st_u[top];
int par = st_par[top];
int i = st_i[top];
if (i < st_sz[top]) {
st_i[top] = i + 1;
int v = g[u][i];
if (dfn[v] == 0) {
// 树边:向下展开,孩子入栈
tick++;
dfn[v] = low[v] = tick;
top++;
st_u[top] = v;
st_par[top] = u;
st_i[top] = 0;
st_sz[top] = g[v].size();
} else if (v != par && dfn[v] < dfn[u]) {
// 回边:无向图对称边只取编号小的一端,防止把父方向的边当回边
if (dfn[v] < low[u]) low[u] = dfn[v];
}
} else {
// u 的邻边走完:出栈并向父结算
top--;
if (par >= 0) {
if (low[u] < low[par]) low[par] = low[u];
if (low[u] >= dfn[par]) {
// u 的子树绕不开 par,删掉 par 后 u 的子树自成一块
split_cnt[par]++;
}
}
// 非根删点后 = 分离孩子数 + 父方向那一大块(+1);根只有各子树(不加 1)
int pieces = split_cnt[u] + (par >= 0);
if (pieces > best) best = pieces;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
while (cin >> n >> m) {
if (n == 0) break; // 读入以 0 0 结束
for (int i = 0; i < n; i++) {
g[i].clear();
dfn[i] = 0;
split_cnt[i] = 0;
}
for (int i = 1; i <= m; i++) { // 保证无重边,直接双向建表
int a, b;
cin >> a >> b;
add_edge(a, b);
}
tick = 0;
comps = 0;
best = 0;
for (int i = 0; i < n; i++) {
if (dfn[i] == 0) { // 未访问点才启动新 DFS(兜底处理多个连通块)
comps++;
dfs(i);
}
}
// 删掉 v 不影响 v 之外的连通块:总数 = 其余块 (comps-1) + v 所在块裂成的块数
cout << comps - 1 + best << "\n";
}
return 0;
}复杂度
- 时间复杂度:每组数据
。每个顶点入栈、出栈各一次,每条边被两端各扫描一次;外层逐点开搜只是兜底,未访问点才启动新 DFS,总代价线性。多组数据合计与输入规模同阶,把朴素的 遍重扫压成 1 遍。 - 空间复杂度:
,邻接表 条记录, dfn/low/split各个槽位,栈最深 ;读入逐行惰性切分,不驻留整份输入。 - 实测(真实数据
e1.in,OJ 限时 1000 ms / 128 MB):0.47 s、峰值 19.2 MB,无 TLE/MLE 风险。
总结
- 答案 = 原连通块数
+ 删点最大裂块数:把「删谁」的枚举转化为「每个点会被裂成几块」的计数,删点的局部性让其余连通块可以整体搬运。 - 一次 Tarjan DFS 同时维护
:非根 ,根 = 子树数,孤立点 = 0;三条规则在代码里统一为 split[u] + (par >= 0)。 - 朴素逐点删点
的瓶颈是每遍重扫全图;「分解 + 判据」把它压成一遍 。 - 实现上两点:读入逐行流式切分(7+ MB 输入峰值仅约 19 MB),深图用显式栈迭代而非递归。