概述
- 选择树中任何一个节点作为根,求可以构造最小高度的树的根节点的结点编号
分析
搜索法
根据题目,容易想到通过选择不同的结点来构造树,然后利用树的层次遍历(即BFS)来获得该树的高度
结论法
实际上,该题是个结论题,需要一定的背景知识
结论:
设 $\textit{dist}[x][y] 表 示 从 节 点 x 到 节 点 y 的 距 离 , 假 设 树 中 距 离 最 长 的 两 个 节 点 为 表示从节点 x到节点 y的距离,假设树中距离最长的两个节点为 表示从节点x到节点y的距离,假设树中距离最长的两个节点为 (x,y)$,它们之间的距离为 maxdist = dist [ x ] [ y ] \textit{maxdist} = \textit{dist}[x][y] maxdist=dist[x][y],则可以推出以任意节点构成的树最小高度一定为 m i n h e i g h t = ⌈ m a x d i s t 2 ⌉ minheight=\left \lceil \dfrac{maxdist}{2} \right \rceil minheight=⌈2maxdist⌉,且最小高度的树根节点一定在 节点 x到节点 y的路径上。
-
证明思路大致如下:
将最长链称之为主链。主链上任意节点可以将主链分为包含该节点的上下两部分,可以有以下几个结论:
-
该节点的任意旁路分支最大深度都不能超过主链短的那部分的长度,否则最长链选择错误
-
以该节点为根的树的最大深度为主链长的那部分的长度
如果不等于,则与最长链矛盾
-
以该节点旁路分支上的任意节点的为根的树高度不会小于 主链长的部分+1,所以不可以选择非主链上的节点作为根
-
选择中点为根是主链较长部分长度最小的策略,也就是最优解
-
思路
搜索法
-
根据输入的形式,如何选择保存图的结构?
- 这里建议使用邻接表法,因为数据量很大 2 ∗ 1 0 4 2*10^4 2∗104,如果使用邻接矩阵空间占用较大
-
如何保存每个结点作为根获得的树高,并且最终得到最小高度的根结点?
-
因为可能有多个解,所以选择
std::map<int, vector<int>> res_record;
第一位记录高度,第二位记录根结点编号
-
结论法
-
如何找到树中距离最长的两个结点呢?
-
这里也有一个结论:从任意一个点出发找到最远的点P 从P点出发找到最远的点Q P Q之间的距离即为树中最远两个点距离
-
证明大概思路参考该篇博客:Leetcode 树的直径(树中最远的两个点)
-
-
-
如何获得两个最远结点的中间的路径呢?
- 一种思路是找到确定两个结点后,再利用搜索策略,获得路径
- 其实可以在找两个结点的过程中记录该路径,具体实现可以参考下面代码注释
代码
搜索代码
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
vector<vector<int>> map(n, vector<int>(n));
for (auto &p : edges) { // 邻接矩阵保存图
map[p[0]][p[1]] = 1;
map[p[1]][p[0]] = 1;
}
auto temp_map = map; // 邻接矩阵副本
std::map<int, vector<int>> res_record;
for (int i = 0; i < n; ++i) { // 遍历每个结点,作为树更
queue<int> record;
record.push(i);
int height = -1;
while(!record.empty()) { // BFS获取深度
++height; // 记录深度
int size = record.size();
while(size--) {
int t = record.front(); record.pop();
for (int j = 0; j < n; ++j) { // 广度遍历该结点的所有邻接节点
if (map[t][j] == 1) {
map[j][t] = 0;
record.push(j);
}
}
}
}
res_record[height].push_back(i); // 记录该高度下的根结点
map = temp_map;
}
return res_record.begin()->second; // 输出最小高度的根结点
}
};
-
我们知道使用邻接矩阵保存的图,进行搜索,其复杂度为 O ( n 2 ) O(n^2) O(n2),而我们还要循环每个结点作为根,那么复杂度就是 O ( n 3 ) O(n^3) O(n3),肯定超时
即使使用邻接表保存图,搜索的复杂度也有 O ( n ) O(n) O(n),整个算法复杂度也是 O ( n 2 ) O(n^2) O(n2),在大规模数据下也会超时
结论法
官方题解代码
class Solution {
public:
int findLongestNode(int u, vector<int> & parent, vector<vector<int>>& adj) {
int n = adj.size();
queue<int> qu;
qu.emplace(u);
vector<bool> visit(n);
visit[u] = true;
int node = -1;
while (!qu.empty()) {
int curr = qu.front();
qu.pop();
node = curr;
for (auto & v : adj[curr]) {
if (!visit[v]) {
visit[v] = true;
parent[v] = curr; // 记录路径
qu.emplace(v);
}
}
}
return node;
}
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
if (n == 1) { // 特例,只有一个结点
return {0};
}
vector<vector<int>> adj(n); // 注意要用邻接表保存图
for (auto & edge : edges) {
adj[edge[0]].emplace_back(edge[1]);
adj[edge[1]].emplace_back(edge[0]);
}
vector<int> parent(n, -1); // 用来在确定最远节点时保存路径
/* 找到与节点 0 最远的节点 x */
int x = findLongestNode(0, parent, adj);
/* 找到与节点 x 最远的节点 y */
int y = findLongestNode(x, parent, adj);
/* 求出节点 x 到节点 y 的路径 */
vector<int> path;
parent[x] = -1;
while (y != -1) {
path.emplace_back(y);
y = parent[y];
}
// Find(x,y,false)
int m = path.size();
if (m % 2 == 0) {
return {path[m / 2 - 1], path[m / 2]};
} else {
return {path[m / 2]};
}
}
};
-
这里解释以下
parent
的作用- 在通过某个结点找其相邻的节点时,我们通过
paretn[v]=cur
反向记录了节点v与cur相连,可以确保所有与cur相连的路径都被记录,然后可以反向找到原结点 - 如果是
parent[cur]=v
,则只能记录cur和一个节点的连接情况,而该连接不一定是最长路径的的连接
这里用这种方法比较难想,一般我们用第一种思路:再使用一次搜索来记录路径 就可以了
- 在通过某个结点找其相邻的节点时,我们通过