C++ 树形结构 k叉树最优根节点选择问题
参考资料
问题描述
题目背景
对于一棵有根树,如果它的全部节点都满足:孩子个数不多于 k,且 k 是所有可取的值中最小的那个,则称它是一个 k 叉树。
形式化的,记 cnt_i 为有根树上,第 i 个节点的孩子数目,则:
k = max{cnt_1, cnt_2, ..., cnt_n}
现在智乃想要给一棵由 n 个节点构成的无根树标记一个根节点,她发现选择的根节点不同时,k 的值也不同。
任务:选择一个节点作为根节点,以最小化 k 的取值,并输出最小的 k 值和对应的根节点编号(多个时选编号最小的)。
输入格式
- 第一行:一个正整数 n (2 ≤ n ≤ 10^5),代表树的节点数量
- 接下来 n-1 行:每行两个正整数 u_i, v_i,代表无根树上的一条无向边
输出格式
输出两个正整数:最小的 k 值和对应的根节点编号
示例
示例 1:
输入:
4
3 1
2 3
3 4
输出:
2 1
示例 2:
输入:
3
1 2
1 3
输出:
1 2
问题分析
核心观察
在理解这道题之前,我们需要明确几个关键概念:
无根树与有根树的区别
- 无根树:所有节点地位平等,边是无向的
- 有根树:指定一个根节点后,其他节点按层次组织,边有方向(父→子)
度数的定义
- 在无根树中,节点的度数 = 连接该节点的边数
- 在有根树中,节点的孩子数取决于它是否为根节点
根节点对 k 值的影响
当我们选择节点 r 作为根时:
- 根节点 r 的孩子数 = degree[r](所有连接的边都指向孩子)
- 非根节点 u 的孩子数 = degree[u] - 1(有一条边连向父节点)
因此:
k_r = max(degree[r], max{degree[u] - 1 | u ≠ r})
问题转化
设:
max_deg= 所有节点中的最大度数second_max_deg= 次大度数
对于节点 r,我们可以分情况讨论:
情况 1:degree[r] < max_deg
k_r = max(degree[r], max_deg - 1)
因为最大度数节点不是根,它会损失一个孩子。
情况 2:degree[r] == max_deg
- 如果只有 r 一个节点有最大度数:
k_r = max(max_deg, second_max_deg - 1) - 如果有多个节点有最大度数:
k_r = max_deg
最优策略
通过上述分析,我们发现:
- 选择度数最大的节点作为根,通常能获得较小的 k 值
- 但需要考虑次大度数的影响
解题思路
算法流程
1. 统计每个节点的度数
2. 找出最大度数 max_deg 和次大度数 second_max_deg
3. 统计有多少个节点的度数等于 max_deg
4. 对每个节点 r,根据公式计算 k_r
5. 选择 k_r 最小的节点(相同时选编号最小的)
伪代码
输入 n 和 n-1 条边
初始化 degree 数组
for 每条边 (u, v):
degree[u]++
degree[v]++
找出 max_deg, second_max_deg, max_count
for r from 1 to n:
if degree[r] < max_deg:
k_r = max(degree[r], max_deg - 1)
else if degree[r] == max_deg:
if max_count == 1:
k_r = max(max_deg, second_max_deg - 1)
else:
k_r = max_deg
更新最优答案
输出 min_k 和 best_root
代码实现
完整代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> degree(n + 1, 0);
// 读入边并统计度数
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
degree[u]++;
degree[v]++;
}
// 找出最大度数和次大度数
int max_deg = 0, second_max_deg = 0;
int max_count = 0; // 有多少个节点的度数等于max_deg
for (int i = 1; i <= n; i++) {
if (degree[i] > max_deg) {
second_max_deg = max_deg;
max_deg = degree[i];
max_count = 1;
} else if (degree[i] == max_deg) {
max_count++;
} else if (degree[i] > second_max_deg) {
second_max_deg = degree[i];
}
}
// 计算每个节点作为根时的k值
int min_k = INT_MAX;
int best_root = 1;
for (int r = 1; r <= n; r++) {
int k_r;
if (degree[r] < max_deg) {
// r不是最大度数节点
k_r = max(degree[r], max_deg - 1);
} else {
// degree[r] == max_deg
if (max_count == 1) {
// r是唯一的最大度数节点
k_r = max(max_deg, second_max_deg - 1);
} else {
// 有多个最大度数节点
k_r = max_deg;
}
}
// 更新最优答案(k值小的优先,相同时编号小的优先)
if (k_r < min_k || (k_r == min_k && r < best_root)) {
min_k = k_r;
best_root = r;
}
}
cout << min_k << " " << best_root << endl;
return 0;
}
代码说明
关键变量:
degree[i]:节点 i 的度数max_deg:所有节点中的最大度数second_max_deg:次大度数max_count:度数等于最大度数的节点数量
核心逻辑:
- 度数统计(第 10-16 行):遍历所有边,统计每个节点的度数
- 找最大值和次大值(第 18-31 行):一次遍历找出最大和次大度数
- 计算 k 值(第 33-52 行):根据节点度数与最大度数的关系计算 k_r
- 更新答案(第 54-57 行):保持最小 k 值和最小编号
示例验证
示例 1 详解
输入:
4
3 1
2 3
3 4
树的结构(无根树):
1
|
3---2
|
4
度数统计:
- degree[1] = 1
- degree[2] = 1
- degree[3] = 3 ← 最大度数
- degree[4] = 1
各节点作为根时的 k 值:
| 根节点 | degree[r] | 计算公式 | k_r | 说明 |
|---|---|---|---|---|
| 1 | 1 | max(1, 3-1) | 2 | 节点3有2个孩子 |
| 2 | 1 | max(1, 3-1) | 2 | 节点3有2个孩子 |
| 3 | 3 | max(3, 1-1) | 3 | 节点3本身有3个孩子 |
| 4 | 1 | max(1, 3-1) | 2 | 节点3有2个孩子 |
结论: min_k = 2,最小编号 = 1,输出 2 1 ✓
示例 2 详解
输入:
3
1 2
1 3
树的结构:
2
\
1
/
3
度数统计:
- degree[1] = 2 ← 最大度数
- degree[2] = 1
- degree[3] = 1
各节点作为根时的 k 值:
| 根节点 | degree[r] | 计算公式 | k_r | 说明 |
|---|---|---|---|---|
| 1 | 2 | max(2, 1-1) | 2 | 节点1有2个孩子 |
| 2 | 1 | max(1, 2-1) | 1 | 节点1只有1个孩子了 |
| 3 | 1 | max(1, 2-1) | 1 | 节点1只有1个孩子了 |
结论: min_k = 1,最小编号 = 2,输出 1 2 ✓
复杂度分析
时间复杂度
- 读入边并统计度数:O(n)
- 找最大和次大度数:O(n)
- 枚举所有节点计算 k 值:O(n)
总时间复杂度:O(n)
空间复杂度
degree数组:O(n)- 其他变量:O(1)
总空间复杂度:O(n)
优化点
本算法已经是最优解,无需进一步优化:
- 不需要真正建立树结构
- 不需要 DFS/BFS 遍历
- 只需统计度数即可得出答案
扩展思考
变体问题
如果要输出所有最优根节点怎么办?
- 使用 vector 存储所有 k 值等于 min_k 的节点
如果树的节点数达到 10^6 级别?
- 当前算法依然高效,O(n) 可以轻松处理
如果要求输出构造后的树结构?
- 需要真正建图并进行 DFS/BFS,复杂度仍为 O(n)
相关问题
总结
核心知识点
- ⭐ 度数是关键:无需建树,只需统计度数
- ⭐ 公式推导:根节点与非根节点的孩子数计算方式不同
- ⭐ 分类讨论:根据节点度数与最大度数的关系分情况处理
- ⭐ 贪心思想:选择度数大的节点作为根通常更优
易错点
- ❌ 忘记处理多个节点有相同最大度数的情况
- ❌ 没有正确计算次大度数
- ❌ 输出时忘记优先选择编号小的节点
- ❌ 边界情况:链状树(退化为 1 叉树)
学习建议
- 理解无根树与有根树的转换关系
- 掌握度数的概念和应用
- 学会用数学公式简化树形问题
- 多做树形结构相关的题目
参考代码模板
// 树形问题通用模板
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> degree(n + 1, 0);
// 1. 统计度数
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
degree[u]++;
degree[v]++;
}
// 2. 找极值
int max_deg = *max_element(degree.begin() + 1, degree.end());
// 3. 枚举计算
int ans = INT_MAX;
int pos = 1;
for (int i = 1; i <= n; i++) {
int cur = /* 根据问题计算 */;
if (cur < ans || (cur == ans && i < pos)) {
ans = cur;
pos = i;
}
}
cout << ans << " " << pos << endl;
return 0;
}