本文已参与「新人创作礼」活动,一起开启掘金创作之路。
题目
解法一:枚举根节点+DFS求树高(超时)
枚举以每个节点为根构成的树,然后求出该树的高度,所有树的最小高度即为答案,需要的时间复杂度为
算法流程:
-
从到枚举根节点
- 利用
dfs求出以为根节点的树高,并保存
- 利用
-
枚举所有树高,找出最小高度
class Solution {
// 邻接表
private Set<Integer>[] adj;
// 以 i 为根节点时树的高度
private int[] height;
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
adj = new HashSet[n];
for (int i = 0; i < n; i++) {
adj[i] = new HashSet<>();
}
height = new int[n];
// 获得邻接表
for (int[] edge : edges) {
int a = edge[0], b = edge[1];
adj[a].add(b);
adj[b].add(a);
}
int minHeight = n;
// 枚举根节点并求得树高
for (int i = 0; i < n; i++) {
boolean[] visited = new boolean[n];
height[i] = dfs(i, visited, 0);
minHeight = Math.min(minHeight, height[i]);
}
// 枚举树高,找出最小高度
List<Integer> res = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (minHeight == height[i]) {
res.add(i);
}
}
return res;
}
// 求以v为根节点的树高
private int dfs(int v, boolean[] visited, int h) {
visited[v] = true;
int maxH = h;
for (int w : adj[v]) {
if (!visited[w]) {
maxH = Math.max(dfs(w, visited, h + 1), maxH);
}
}
return maxH;
}
}
解法二:广度优先搜索
根据题意,有以下结论成立:
设两个叶子节点和的最长距离为 maxdist,可以得到结论最小高度树的高度为 ,且最小高度树的根节点一定在 节点 到节点 的路径上。假设最长的路径的 m 个节点依次为 ,最长路径的长度为 ,则:
- 如果 为偶数,此时最小高度树的根节点为 或者 ,且此时最小的高度为 ;
- 如果 为奇数,此时最小高度树的根节点为 ,且此时最小的高度为 。
上述结论的证明参见官方题解方法一
可以利用以下算法找到图中距离最远的两个节点与它们之间的路径:
- 从任意节点 出发 ,利用广度优先搜索或者深度优先搜索找到以 为起点的最长路径的终点 ;
- 从节点 出发,找到以 为起点的最长路径的终点 ;
- 到 之间的路径即为图中的最长路径,找到路径的中间节点即为根节点
上述算法的证明可以参考「算法导论习题解答 9-1」
算法流程:
- 首先利用广度优先搜索找到距离节点 的最远节点
- 然后找到距离节点 的最远节点
- 然后找到节点 与节点 的路径的最中间的节点即为最小高度树的根节点
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<Integer> ans = new ArrayList<>();
if (n == 1) {
ans.add(0);
return ans;
}
// 邻接表
List<Integer>[] adj = new List[n];
for (int i = 0; i < n; i++) {
adj[i] = new ArrayList<>();
}
for (int[] edge : edges) {
adj[edge[0]].add(edge[1]);
adj[edge[1]].add(edge[0]);
}
// x->y路径中,存储每一个顶点的前一个顶点
int[] parent = new int[n];
Arrays.fill(parent, -1);
// 找到与节点 0 最远的节点 x
int x = findLongestNode(0, parent, adj);
// 找到与节点 x 最远的节点 y
int y = findLongestNode(x, parent, adj);
// 求出节点 x 到节点 y 的路径
List<Integer> path = findPath(parent, x, y);
// path中没有加入节点x,此时path.size()就是路径长度
int m = path.size();
if (m % 2 == 0) {
ans.add(path.get(m / 2 - 1));
}
ans.add(path.get(m / 2));
return ans;
}
private List<Integer> findPath(int[] parent, int x, int y) {
List<Integer> path = new ArrayList<>();
parent[x] = -1;
while (y != -1) {
path.add(y);
y = parent[y];
}
return path;
}
private int findLongestNode(int u, int[] parent, List<Integer>[] adj) {
int n = adj.length;
Queue<Integer> queue = new ArrayDeque<>();
boolean[] visited = new boolean[n];
queue.offer(u);
visited[u] = true;
// 距离节点u的最远节点
int node = -1;
while (!queue.isEmpty()) {
int cur = queue.poll();
node = cur;
for (int v : adj[cur]) {
if (!visited[v]) {
visited[v] = true;
parent[v] = cur;
queue.offer(v);
}
}
}
return node;
}
}
- 时间复杂度:,其中 是节点的个数
- 空间复杂度:
解法三:拓扑排序
根据题意,越是靠里面的节点越有可能是最小高度树的根节点,距离两边相同的节点相当于把整个距离二分了,当然就是到两边距离最小的节点了
算法流程如下:
- 首先找到所有度为 1 的节点压入队列,此时令节点剩余计数
- 同时将当前 计数减去出度为 1 的节点数目,将最外层的度为 1 的叶子节点取出,并将与之相邻的节点的度减少,重复上述步骤将当前节点中度为 1 的节点压入队列中
- 重复上述步骤,直到剩余的节点数组 时,此时剩余的节点即为当前高度最小树的根节点
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<Integer> ans = new ArrayList<>();
if (n == 1) {
ans.add(0);
return ans;
}
// 记录每个节点的度
int[] degree = new int[n];
// 邻接表
List<Integer>[] adj = new List[n];
for (int i = 0; i < n; i++) {
adj[i] = new ArrayList<>();
}
for (int[] edge : edges) {
adj[edge[0]].add(edge[1]);
adj[edge[1]].add(edge[0]);
degree[edge[0]]++;
degree[edge[1]]++;
}
Queue<Integer> queue = new ArrayDeque<>();
// 首先找到所有度为 1 的节点压入队列
for (int i = 0; i < n; i++) {
if (degree[i] == 1) {
queue.offer(i);
}
}
// 剩余节点数
int remainNodes = n;
while (remainNodes > 2) {
int sz = queue.size();
remainNodes -= sz;
// 将最外层的度为 1 的叶子节点取出
for (int i = 0; i < sz; i++) {
int cur = queue.poll();
for (int v : adj[cur]) {
// 将与之相邻的节点的度减少1
degree[v]--;
if (degree[v] == 1) {
queue.offer(v);
}
}
}
}
while (!queue.isEmpty()) {
ans.add(queue.poll());
}
return ans;
}
}
- 时间复杂度:,其中 为节点的个数
- 空间复杂度:
解法四:树形DP
Reference
\