310. Minimum Height Trees
Leetcode Breath-first Search GraphFor an undirected graph with tree characteristics, we can choose any node as the root. The result graph is then a rooted tree. Among all possible rooted trees, those with minimum height are called minimum height trees (MHTs). Given such a graph, write a function to find all the MHTs and return a list of their root labels.
Format
The graph contains n nodes which are labeled from 0 to n - 1. You will be given the number n and a list of undirected edges (each edge is a pair of labels).
You can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.
Example 1 :
Input: n = 4, edges = [[1, 0], [1, 2], [1, 3]]
0
|
1
/ \
2 3
Output: [1]
Example 2 :
Input: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]
0 1 2
\ | /
3
|
4
|
5
Output: [3, 4]
Note:
- According to the definition of tree on Wikipedia: “a tree is an undirected graph in which any two vertices are connected by exactly one path. In other words, any connected graph without simple cycles is a tree.”
- The height of a rooted tree is the number of edges on the longest downward path between the root and a leaf.
分析¶
这道题目有两类解法。第一种方法是,观察得到以树中的最长路径的中点为根构建的树的高度是最小的。那么问题就变成如何寻找树中的最长路径(longest path of a tree)。其中一种简便的方法就是,使用两次bfs,第一次bfs以任意点出发p_0,寻找到最远的点v,第二次bfs以寻找到的最远点v出发,寻找到距离该点距离最远的点w。路径v-w就是树中的最长路径。可以简单证明如下:第一次bfs寻找到的点一定为最长路径的一个端点,利用反证法,如果存在另一点p为第一次bfs的最远点,那么|p-p_0| > |v-w|,显然与最长路径定义矛盾。既然第一次bfs寻找到了最长路径一个端点,第二次bfs就肯定是另一个端点。
private int[] bfs(Map<Integer, List<Integer>> graph, int[] edgeTo, int s) {
int[] distTo = new int[edgeTo.length];
boolean[] mark = new boolean[edgeTo.length];
Queue<Integer> q = new ArrayDeque<>();
q.offer(s);
mark[s] = true;
distTo[s] = 0;
while (!q.isEmpty()) {
int v = q.poll();
for (int w : graph.get(v)) {
if (!mark[w]) {
mark[w] = true;
distTo[w] = distTo[v] + 1;
edgeTo[w] = v;
q.offer(w);
}
}
}
int longestPath = 0, longestDist = -1;
for (int i = 0; i < distTo.length; i++) {
if (distTo[i] > longestDist) {
longestDist = distTo[i];
longestPath = i;
}
}
return new int[]{longestDist, longestPath};
}
public List<Integer> findMinHeightTrees(int N, int[][] edges) {
// construct graph
Map<Integer, List<Integer>> graph = new HashMap<>();
for (int i = 0; i < N; i++)
graph.put(i, new ArrayList<>());
for (int i = 0; i < edges.length; i++) {
int v = edges[i][0], w = edges[i][1];
graph.get(v).add(w); // v->w
graph.get(w).add(v); // w->v
}
// two bfs to find longest path v-w
int[] edgeTo = new int[N];
int v = bfs(graph, edgeTo, 0)[1];
int tmp[] = bfs(graph, edgeTo, v);
int w = tmp[1], longestDist = tmp[0];
// find roots (mid-points) of longest path v-w
List<Integer> roots = new ArrayList<>();
int mid = w;
for (int i = 0; i < longestDist / 2; i++)
mid = edgeTo[mid];
roots.add(mid);
if (longestDist % 2 == 1) // two middle points
roots.add(edgeTo[mid]);
return roots;
}
而第二种方法非常巧妙了,改自BFS拓扑排序。非常类似“剥洋葱”法BFS:从叶子节点剥向根节点。可以这么设想:最简单的图是什么? a path graph,连成一条直线的图,那么怎么寻找该图的根节点?使用两个指针,一个指向尾部end, 一个指向首部front, 然后依次向中间移动。对于一个任意无向图,这样的path graph有很多,那么指针的前后向中间移动,可以抽象成一个外面的面向中间收缩,也就是剥洋葱了。
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<List<Integer>> graph = new ArrayList<List<Integer>>();
List<Integer> roots = new ArrayList<Integer>();
// special case: one vertex
if (n == 1) { roots.add(0); return roots; }
// degree of every vertex
int[] degree = new int[n];
for(int i = 0; i< n; i++) graph.add(new ArrayList<Integer>());
// initialize degree and graph
for(int i=0; i<edges.length; i++) {
int v = edges[i][0], w = edges[i][1];
graph.get(v).add(w);
graph.get(w).add(v);
degree[v]++;
degree[w]++;
}
// add leaves
Queue<Integer> leaves = new ArrayDeque<Integer>();
for(int i = 0; i< n; i++)
if (degree[i] == 1) leaves.offer(i);
while (!leaves.isEmpty()) { //剥一层叶子
roots = new ArrayList<Integer>(); // 根就是最后一层叶子
int leave_size = leaves.size(); // 这层叶子大小,把这层叶子剥掉
for(int i = 0; i < leave_size; i++){
int leaf = leaves.poll();
roots.add(leaf); // 加入叶子
degree[leaf]--; // 叶子的度减去1
for (int next : graph.get(leaf)) { // 遍历连接叶子的节点
if (degree[next] == 0) continue; // 原本就是叶子,i.e.外层
if (degree[next] == 2) leaves.offer(next); // 把这个节点变成叶子
degree[next]--;
}
}
}
return roots;
}