一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
题目链接:310. 最小高度树
题目描述
树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
提示:
1 <= n <= 2 * 10^4edges.length == n - 10 <= ai, bi < nai != bi- 所有
(ai, bi)互不相同 - 给定的输入 保证 是一棵树,并且 不会有重复的边
示例 1:
输入:n = 4, edges = [[1,0],[1,2],[1,3]]
输出:[1]
解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。
示例 2:
输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]
输出:[3,4]
整理题意: 题目给了一棵多叉树,要求我们找到使得树的高度最小的根节点(可能不止一个)。
解题思路分析
- 首先我们能想到最暴力的解法就是枚举每个节点作为根节点时求其树的高度,然后记录每个节点作为根节点时树的高度,其中高度最小的根节点即为答案。但是我们发现题目所给的数据
1 <= n <= 2 * 10^4显然这样暴力的做法时间复杂度是承受不了的。 - 进一步分析可知,我们需要找到一个中间节点,这个节点到达所有叶子节点的距离中最大的距离即为树的高度,为了使得这个高度最小,也就是使得最大距离最小,我们可以想到最短路(一般遇到图和树的题,通常情况都会使用到搜索和遍历)。
- 现在考虑如何求最短路,如果遍历每个节点去找叶子节点,分别求最短路,那么就回归到情况1的暴力做法,我们可以 反向思考,从每个叶子节点出发同时去找根节点 ,我们可以想到 多源BFS ,以每个叶子节点作为起始节点,同时向周围扩散,最后到达的节点即为我们所求的节点。
- 但是这样做存在一个问题,每个叶子节点的深度可能不一致,那么对于某个父亲节点可能被遍历到多次,我们因当取最后遍历的那一次,这是因为我们要优先考虑较远的叶子节点(也就是最大距离的最小值中的最大距离是我们需要优先考虑的),那么遍历的时候我们还需要记录和维护每个节点的度,只有当节点的度为
1的时候我们才遍历(也就是叶子节点的状态),这样能确保该节点不含有其它儿子节点,仅含有一个父亲节点,那么我们可以把这个操作看成 每次删除树的叶子节点,对于删除叶子节点后的树,再进行删除叶子节点,依次循环 最后找到的节点就是边缘同时向中间靠近的节点,那么这个中间节点就相当于把整个距离二分了,那么它当然就是到两边最远叶子节点距离最小的节点啦,也就是最大距离的最小值了。 - 接下来就是代码实现了,我们可以在多源BFS的基础上维护每个节点的度数,这里用
degree[i]表示节点i的度数,首先建图,在建图的同时记录每个节点的度数,然后将度数为1的叶子节点压入BFS的队列,每次从队首取出的节点表示要删除的叶子节点,这时候需要将它的父亲节点度数减一操作,如果父亲节点的度数在减一操作后度数也变成了1表示在删除当前叶子节点后,该节点的父亲节点将成为下一轮的叶子节点,这时候我们需要将其压入BFS的队列中,依次循环,同时记录每个节点入队的时间,最后入队时间最大的节点即为我们所求的节点。
代码实现
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
//degree[i]记录节点i的度(无向图)
int degree[n];
memset(degree, 0, sizeof(degree));
//mp[i]记录节点i是否遍历过
int mp[n];
memset(mp, 0, sizeof(mp));
//建图
vector<vector<int>> G;
//初始化图
G.resize(n);
G.clear();
for(int i = 0; i < n - 1; i++){
//记录节点的度
degree[edges[i][0]]++;
degree[edges[i][1]]++;
//建边
G[edges[i][0]].push_back(edges[i][1]);
G[edges[i][1]].push_back(edges[i][0]);
}
//多源bfs,从叶子节点出发
queue<pair<int, int> > que;
//first表示节点值,second表示节点步数
while(que.size()) que.pop();
for(int i = 0; i < n; i++){
//度为1的节点即为叶子节点
if(degree[i] == 1){
//标记节点
mp[i] = 1;
que.push(make_pair(i, 0));
}
}
//step[i]记录到达节点i的步数
int step[n];
memset(step, 0, sizeof(step));
while(que.size()){
//记录当前节点
int now = que.front().first;
//记录节点的步数
step[now] = que.front().second;
que.pop();
for(int i = 0; i < G[now].size(); i++){
//将该节点所连接的节点度数减一,因为要删除该节点
degree[G[now][i]]--;
//如果所连接的节点度数也为1,那么表示这是新的叶子节点,需要入队,同时还需要标记
if(degree[G[now][i]] == 1 && !mp[G[now][i]]){
//标记节点
mp[G[now][i]] = 1;
//压入队列
que.push(make_pair(G[now][i], step[now] + 1));
}
}
}
//寻找最大步数的节点
int maxStep = 0;
for(int i = 0; i < n; i++) maxStep = max(maxStep, step[i]);
//记录答案
vector<int> ans;
ans.clear();
for(int i = 0; i < n; i++){
//步数为最大值的节点即为答案
if(step[i] == maxStep) ans.push_back(i);
}
return ans;
}
};
总结
在思考题目的时候,发现正向思维比较复杂或者难以解决的时候,我们可以反向思考(正繁则反的思维),往往能够发现题目简单的一面。
该题还有有多种解决办法,包括严谨的数学证明等:
- 设 表示从节点 到节点 的距离,假设树中距离最长的两个节点为 ,它们之间的距离为 ,则可以推出以任意节点构成的树最小高度一定为 ,且最小高度的树根节点一定在 节点 到节点 的路径上。
- 设两个叶子节点的最长距离为 ,可以得到结论最小高度树的高度为 ,且最小高度树的根节点一定存在其最长路径上。
结束语
生活就是不断突破自我的过程。我们努力地向上,不仅是让世界看到我们,更是为了让自己看到世界。当我们一步一个脚印往前走时会发现,每一点进步,都在让我们的人生变得辽阔。