题解:洛谷 AT_agc001_c Shorten Diameter
【题目来源】
洛谷:AT_agc001_c [AGC001C] Shorten Diameter - 洛谷
【题目描述】
给你一棵 \(N\) 个点的无向树,定义点 \(u\) 和 \(v\) 之间的距离是从 \(u\) 到 \(v\) 的简单路径上的边数。
你需要删除一些点,使树的直径小于等于 \(K\),当且仅当删除某点不会对树的联通性产生影响时才可以删除。问至少删除多少点才可以满足要求。
【输入】
第一行两个整数 \(N, K\)。
之后 \(N - 1\) 行描述一棵树。
【输出】
一个整数,表示最少删掉点的个数。
【输入样例】
6 2
1 2
3 2
4 2
1 6
5 6
【输出样例】
2
【核心思想】
-
问题分析:给定一棵 \(N\) 个节点的无向树,要求删除最少的节点,使得剩余树的直径不超过 \(K\)。删除节点后剩余部分必须保持连通(即只能删除一些“外部”节点,实际上保留的是一个连通子图)。问题等价于:在树中保留一个最大的连通子图,使其直径 \(\le K\),然后 \(答案 = N - 最大保留节点数\)。在树上,任意一个连通子图的直径 \(\le K\) 当且仅当它可以被一个半径 \(\le \lfloor K/2 \rfloor\) 的“中心”覆盖——若 \(K\) 为偶数,中心是一个点;若 \(K\) 为奇数,中心是一条边。因此,只需枚举所有可能的中心点或中心边,统计从中心出发在半径内能覆盖的节点数,取最大值。
-
算法选择:
- 枚举中心(点/边)+ DFS 统计:由于 \(N \le 2000\),可以枚举所有可能的中心点(\(N\) 个)和中心边(\(N-1\) 条),对每个中心执行 DFS 统计距离不超过给定半径的节点数。取最大值作为最多能保留的节点数。
- 分奇偶处理:
- 若 \(K\) 为偶数,中心为一个点,半径 \(r = K/2\),保留所有到该点距离 \(\le r\) 的节点。
- 若 \(K\) 为奇数,中心为一条边 \((u, v)\),半径 \(r = (K-1)/2\),保留所有到 \(u\) 距离 \(\le r\) 且不经过 \(v\) 的节点,以及所有到 \(v\) 距离 \(\le r\) 且不经过 \(u\) 的节点。
- 正确性:任意直径 \(\le K\) 的连通子图必定可以被某个点或某条边(作为中心)覆盖,且上述枚举覆盖了所有可能的中心,因此能找出最大的保留集合。
-
关键步骤:
- 读入与建图:读入 \(N, K\) 和 \(N-1\) 条边,构建无向邻接表。
- 初始化:
max_keep = 0,表示最多能保留的节点数。 - 情况1:\(K\) 为偶数:
- 枚举每个节点 \(i\) 作为中心。
- 从 \(i\) 出发 DFS,深度限制为 \(K/2\),统计能访问到的节点数
tot。 - 更新
max_keep = max(max_keep, tot)。
- 情况2:\(K\) 为奇数:
- 枚举每条无向边 \((u, v)\) 作为中心边。
- 从 \(v\) 出发 DFS(禁止经过 \(u\)),深度限制为 \((K-1)/2\),统计节点数。
- 从 \(u\) 出发 DFS(禁止经过 \(v\)),深度限制为 \((K-1)/2\),统计节点数。
- 两者之和即为以该边为中心能保留的节点数,更新
max_keep。
- 输出答案:
N - max_keep。
-
时间/空间复杂度:
- 时间复杂度:\(O(N^2)\),枚举 \(O(N)\) 个中心(点或边),每次 DFS 遍历 \(O(N)\) 个节点,\(N \le 2000\),可接受。
- 空间复杂度:\(O(N)\),存储树和辅助栈。
-
枚举中心 + DFS 的核心思想:
- 直径与半径的等价性:树上连通子图的直径 \(\le K\) 等价于存在一个中心(点或边)使得子图中所有节点到中心的距离 \(\le \lfloor K/2 \rfloor\)(对奇数情况,中心为边,两端分别控制两侧)。
- 覆盖最大化:为了保留尽可能多的节点,应选择能够覆盖最多节点的中心。直接枚举所有可能的中心即可找到最优解,无需复杂的树形 DP。
- 适用场景:适用于 \(N\) 较小(\(\le 2000\))且需要删除最少节点使树的直径减小的题目,是暴力搜索与树的性质结合的经典思路。
【算法标签】
普及+ #树形DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 2005, M = N * 2; // N: 最大节点数, M: 最大边数(无向图双向边)
int n, k; // n: 节点数, k: 直径限制
int h[N], e[M], ne[M], idx; // 链式前向星存图
int tot, ans; // tot: 当前保留的节点数, ans: 最多能保留的节点数
// 链式前向星:添加无向边
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// DFS:从u出发,最多走step步,统计能到达的节点数
// 这些节点构成一个以u为中心、半径为step的"球"
// fa是父节点,防止走回头路
void dfs(int u, int fa, int step)
{
tot++; // 当前节点u被保留
if (step == 0)
return; // 步数用完,不再扩展
// 遍历u的所有邻接点
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (v == fa)
continue; // 跳过父节点,避免走回头路
dfs(v, u, step - 1); // 递归向v扩展,剩余步数减1
}
}
int main()
{
cin >> n >> k; // 读入节点数和直径限制
// 初始化链式前向星
memset(h, -1, sizeof(h));
// 读入n-1条边(树结构)
for (int i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a); // 无向图,双向建边
}
// ========== 核心思路 ==========
// 要使直径 <= K,等价于找到一个最大的节点集合,使得集合内任意两点距离 <= K
// 答案 = n - 最多保留的节点数(即删除最少的点 = 总数 - 最多保留)
// 情况1:K为偶数
// 最优策略:选择一个中心点,保留距离中心不超过 K/2 的所有点
// 这些点构成的子树直径不超过 K(以中心为根,最远的两点距离为 2*(K/2) = K)
if (k % 2 == 0)
{
for (int i = 1; i <= n; i++)
{
tot = 0; // 重置计数
dfs(i, 0, k / 2); // 以i为中心,半径为K/2的球
ans = max(ans, tot); // 更新最多保留的节点数
}
}
// 情况2:K为奇数
// 最优策略:选择一条边作为"中心边",保留两端分别距离该边不超过 (K-1)/2 的点
// 即:以边(u,v)为中心,u侧保留距离u不超过 (K-1)/2 的点,v侧保留距离v不超过 (K-1)/2 的点
// 最远的两点距离 = (K-1)/2 + 1 + (K-1)/2 = K
else
{
for (int u = 1; u <= n; u++)
{
// 遍历u的所有邻接边
for (int i = h[u]; i != -1; i = ne[i])
{
tot = 0; // 重置计数
int v = e[i]; // 邻接点v,边(u,v)作为中心边
// 从v出发(不经过u),保留距离v不超过 (K-1)/2 的点
dfs(v, u, (k - 1) / 2);
// 从u出发(不经过v),保留距离u不超过 (K-1)/2 的点
dfs(u, v, (k - 1) / 2);
ans = max(ans, tot); // 更新最多保留的节点数
}
}
}
// 输出最少删除的点数 = 总点数 - 最多保留的点数
cout << n - ans << endl;
return 0;
}
【运行结果】
6 2
1 2
3 2
4 2
1 6
5 6
2
浙公网安备 33010602011771号