题解:洛谷 P5536 核心城市
【题目来源】
【题目描述】
X 国有 \(n\) 座城市,\(n - 1\) 条长度为 \(1\) 的道路,每条道路连接两座城市,且任意两座城市都能通过若干条道路相互到达,显然,城市和道路形成了一棵树。
X 国国王决定将 \(k\) 座城市钦定为 X 国的核心城市,其余城市为非核心城市。这 \(k\) 座核心城市需满足以下两个条件:
- 这 \(k\) 座城市可以通过道路,在不经过非核心城市的情况下两两相互到达。
- 定义某个非核心城市与这 \(k\) 座核心城市的距离为,这座城市与 \(k\) 座核心城市的距离的最小值。
为了衡量交通状况,国王发明了交通拥堵度,它为所有非核心城市与核心城市的距离中的最大值。
问题来了,如何安排核心城市才能使交通拥堵度最小呢?请输出满足条件的最小交通拥堵度。
【输入】
第一行 \(2\) 个正整数 \(n,k\)。
接下来 \(n - 1\) 行,每行 \(2\) 个正整数 \(u,v\),表示第 \(u\) 座城市与第 \(v\) 座城市之间有一条长度为 \(1\) 的道路。
【输出】
一行一个整数,表示满足条件的最小交通拥堵度。
【输入样例】
6 3
1 2
2 3
2 4
1 5
5 6
【输出样例】
1
【核心思想】
-
问题分析:给定一棵 \(n\) 个节点的树,选择 \(k\) 个核心城市(需连通),最小化非核心城市到核心城市集的最小距离的最大值(交通拥堵度)。这是一个树的直径 + 贪心覆盖问题,关键在于:最优核心城市集是树的直径上的某段连续路径,通过三次DFS找到直径和中心,然后贪心选取覆盖范围最大的节点。
-
算法选择:
- 三次DFS求直径:两次DFS找直径端点,一次DFS记录直径路径
- 树的中心定位:直径中点作为根,具有最小的最大子树深度
- 子树高度计算:以中心为根DFS,计算每个节点的子树高度
ans[u] - 贪心选取:将
ans降序排序,选前 \(k\) 个最大的作为核心城市,第 \(k+1\) 大的即为最小交通拥堵度
-
关键步骤:
- 第一次DFS:从任意点(如1)出发,找到最远点
st(直径一端) - 第二次DFS:从
st出发,找到最远点ed(直径另一端),同时得到直径长度maxx - 记录直径路径:
dfs_2从st到ed记录路径到router[] - 确定中心:
root = router[maxx / 2](直径中点) - 第三次DFS:以
root为根,计算每个节点的子树最大深度max_deep[u]和子树高度ans[u] = max_deep[u] - deep[u] + 1 - 排序输出:
ans降序排序,输出ans[k+1]
- 第一次DFS:从任意点(如1)出发,找到最远点
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),三次DFS \(O(n)\),排序 \(O(n \log n)\)
- 空间复杂度:\(O(n)\),邻接表和多个辅助数组
-
树直径 + 贪心覆盖的核心思想:
- 核心城市的连通性约束:\(k\) 个核心城市必须连通,最优策略是选择直径上的连续段,因为直径是树中最长路径,覆盖直径即可有效控制全局
- 中心根的最优性:以直径中点为根,各子树深度相对均衡,最小化最大子树高度
- 子树高度的意义:
ans[u]表示以 \(u\) 为核心城市时,能覆盖的子树范围(高度)。选ans最大的 \(k\) 个节点,相当于选覆盖能力最强的 \(k\) 个核心 - 第 \(k+1\) 大的含义:前 \(k\) 个最大的
ans被选为核心,它们覆盖范围内的非核心城市距离 \(\leq\) 对应ans。第 \(k+1\) 大的ans是未被选中的城市中覆盖范围最大的,即最小化的最大距离 - 适用于"树上选 \(k\) 个连通点最小化最大距离"问题,核心在于直径性质和贪心覆盖
【算法标签】
普及+ #树形DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005, M = 2 * N;
int n, k, st, ed, t, maxx, root; // n: 节点数,k: 选择的节点数,st: 直径一端,ed: 直径另一端,t: 临时节点,maxx: 最大深度,root: 树的中心
int h[N], e[M], ne[M], idx; // 邻接表存储树
int deep[N], max_deep[N]; // deep: 节点深度,max_deep: 子树最大深度
int router[N], tmp[N], ans[N]; // router: 直径路径,tmp: 临时路径,ans: 每个节点为根的子树最大深度
// 添加无向边
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
// 第一次DFS,求树的直径的一端
void dfs_1(int u, int fa)
{
deep[u] = deep[fa] + 1; // 计算节点深度
if (deep[u] > maxx) // 更新最大深度
{
maxx = deep[u];
t = u; // 记录最深节点
}
for (int i = h[u]; i != -1; i = ne[i]) // 遍历所有邻接点
{
int v = e[i];
if (v == fa) // 跳过父节点
continue;
dfs_1(v, u);
}
}
// 第二次DFS,求直径路径
void dfs_2(int u, int fa, int step)
{
if (u == ed) // 如果到达直径的另一端
{
memcpy(router, tmp, sizeof(router)); // 复制路径
}
for (int i = h[u]; i != -1; i = ne[i]) // 遍历所有邻接点
{
int v = e[i];
if (v == fa) // 跳过父节点
continue;
tmp[step + 1] = v; // 记录路径
dfs_2(v, u, step + 1);
}
}
// 第三次DFS,计算每个节点的最大深度和ans值
void dfs_3(int u, int fa)
{
deep[u] = deep[fa] + 1; // 计算节点深度
max_deep[u] = deep[u]; // 初始化最大深度为当前深度
for (int i = h[u]; i != -1; i = ne[i]) // 遍历所有邻接点
{
int v = e[i];
if (v == fa) // 跳过父节点
continue;
dfs_3(v, u);
max_deep[u] = max(max_deep[u], max_deep[v]); // 更新子树最大深度
}
ans[u] = max_deep[u] - deep[u] + 1; // 计算以u为根的子树高度
}
int main()
{
cin >> n >> k; // 输入节点数和选择的节点数
memset(h, -1, sizeof(h)); // 初始化邻接表
for (int i = 1; i < n; i++) // 输入n-1条边
{
int u, v;
cin >> u >> v;
add(u, v), add(v, u); // 添加无向边
}
t = maxx = 0;
dfs_1(1, 0); // 第一次DFS,找到直径一端
st = t; // 直径起点
t = maxx = 0;
dfs_1(st, 0); // 第二次DFS,找到直径另一端
ed = t; // 直径终点
tmp[0] = st; // 初始化路径起点
dfs_2(st, 0, 0); // 寻找直径路径
root = router[maxx / 2]; // 找到树的中心(直径中点)
memset(deep, 0, sizeof(deep));
dfs_3(root, 0); // 第三次DFS,计算每个节点的ans值
sort(ans + 1, ans + n + 1, greater<int>()); // 将ans从大到小排序
cout << ans[k + 1] << endl; // 输出第k+1大的值
return 0;
}
【运行结果】
6 3
1 2
2 3
2 4
1 5
5 6
1
浙公网安备 33010602011771号