方法一:广度优先搜索
思路与算法
题目中给定的含有 n n n 个节点的树,可以推出含有以下特征:
- 任意两个节点之间有且仅有一条路径;
- 树中共有 n − 1 n-1 n−1 条不同的边;
- 叶子节点的度为 1 1 1,非叶子节点的度至少为 2 2 2;
- 树的高度由根节点到叶子节点的最大距离决定。
最直接的解法是,枚举以每个节点为根构成的树,然后求出该树的高度,所有树的最小高度即为答案,需要的时间复杂度为 O ( n 2 ) O(n^2) O(n2),但会超时。
设 d i s t [ x ] [ y ] dist[x][y] dist[x][y] 表示从节点 x x x 到节点 y y y 的距离,假设树中距离最长的两个节点为 ( x , y ) (x,y) (x,y),它们之间的距离为 maxdist = d i s t [ x ] [ y ] \text{maxdist}=dist[x][y] maxdist=dist[x][y],则可以推出以任意节点构成的树最小高度一定为 minheight = ⌈ maxdist 2 ⌉ \text{minheight}=\lceil \frac{\text{maxdist}}{2}\rceil minheight=⌈2maxdist⌉,且最小高度的树的根节点一定在节点 x x x 到节点 y y y 的路径上(即以路径上的中心节点作为根节点)。
假设最长路径的 m m m 个节点依次为 p 1 → p 2 → ⋯ → p m p_1\rightarrow p_2\rightarrow\cdots\rightarrow p_m p1→p2→⋯→pm,最长路径的长度为 m − 1 m-1 m−1,可以得到以下结论:
- 若 m m m 为偶数,此时最小高度树的根节点为 p m 2 p_{\frac{m}{2}} p2m 或 p m 2 + 1 p_{\frac{m}{2}+1} p2m+1,且此时最小高度为 m 2 \frac{m}{2} 2m;
- 若 m m m 为奇数,此时最小高度树的根节点为 p m + 1 2 p_{\frac{m+1}{2}} p2m+1,且此时的最小高度为 m − 1 2 \frac{m-1}{2} 2m−1。
因此,我们只需要求出路径最长的两个叶子节点,并求出其路径的中心节点即为最小高度树的根节点。
可以利用以下算法找到图中距离最远的两个节点与它们之间的路径:
- 以任意节点 p p p 出现,利用广度优先搜索或者深度优先搜索找到以 p p p 为起点的最长路径的终点 x x x;
- 以节点 x x x 出发,找到以 x x x 为起点的最长路径的终点 y y y;
- x x x 到 y y y 之间的路径即为图中的最长路径,找到路径的中间节点即为根节点。
代码
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<Integer> ans = new ArrayList<Integer>();
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<Integer>();
}
for(int[] edge: edges){ //初始化邻接表
adj[edge[0]].add(edge[1]);
adj[edge[1]].add(edge[0]);
}
int[] parent = new int[n]; //父亲数组
Arrays.fill(parent, -1);
int x = findLongestNode(0, parent, adj); //找到与节点 0 最远的节点 x
int y = findLongestNode(x, parent, adj); //找到与节点 x 最远的节点 y
List<Integer> path = new ArrayList<Integer>(); //求出节点 x 到节点 y 的路径
parent[x] = -1; //x为根节点,父节点置为-1
while (y != -1) { //当y存在,就将最远路径(x->y)记录下来
path.add(y);
y = parent[y];
}
int m = path.size(); //m为最长路径的节点数
if (m % 2 == 0) { //m为偶数
ans.add(path.get(m / 2 - 1)); //减1是因为节点编号从0开始
}
ans.add(path.get(m / 2)); //m为奇数
return ans;
}
/*
* 利用广度优先算法,以节点u为根节点,找到距离其最远的叶子节点
*/
public int findLongestNode(int u, int[] parent, List<Integer>[] adj) {
int n = adj.length;
Queue<Integer> queue = new ArrayDeque<Integer>();
boolean[] visit = new boolean[n]; //访问数组,防止重复访问
queue.offer(u);
visit[u] = true;
int node = -1;
while (!queue.isEmpty()) {
int curr = queue.poll();
node = curr;
for (int v : adj[curr]) { //依次遍历当前节点的邻接点
if (!visit[v]) { //若未被访问过,则访问,并更新父亲数组
visit[v] = true;
parent[v] = curr;
queue.offer(v);
}
}
}
return node;
}
}
方法二:深度优先搜索
方法一中使用广度优先搜索求出路径最长的节点与路径,我们还可以使用深度优先搜索来实现。首先找到距离节点 0 0 0 的最远节点 x x x,然后找到距离节点 x x x 的最远节点 y y y,然后找到节点 x x x 与节点 y y y 的路径,然后找到根节点。
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<Integer> ans = new ArrayList<Integer>();
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<Integer>();
}
for(int[] edge: edges){ //初始化邻接表
adj[edge[0]].add(edge[1]);
adj[edge[1]].add(edge[0]);
}
int[] parent = new int[n]; //父亲数组
Arrays.fill(parent, -1);
int x = findLongestNode(0, parent, adj); //找到与节点 0 最远的节点 x
int y = findLongestNode(x, parent, adj); //找到与节点 x 最远的节点 y
List<Integer> path = new ArrayList<Integer>(); //求出节点 x 到节点 y 的路径
parent[x] = -1; //x为根节点,父节点置为-1
while (y != -1) { //当y存在,就将最远路径(x->y)记录下来
path.add(y);
y = parent[y];
}
int m = path.size(); //m为最长路径的节点数
if (m % 2 == 0) { //m为偶数
ans.add(path.get(m / 2 - 1)); //减1是因为节点编号从0开始
}
ans.add(path.get(m / 2)); //m为奇数
return ans;
}
/*
* 利用深度优先算法,以节点u为根节点,找到距离其最远的叶子节点
*/
public int findLongestNode(int u, int[] parent, List<Integer>[] adj) {
int n = adj.length;
int[] dist = new int[n]; //记录每个节点到节点u的距离
Arrays.fill(dist, -1);
dist[u] = 0;
dfs(u, dist, parent, adj); //深度优先来更新距离数组
int maxdist = 0;
int node = -1;
for(int i = 0; i < n; i++){ //寻找最大距离以及对应节点
if(dist[i] > maxdist){
maxdist = dist[i];
node = i;
}
}
return node;
}
public void dfs(int u, int[] dist, int[] parent, List<Integer>[] adj) {
for (int v : adj[u]) {
if (dist[v] < 0) {
dist[v] = dist[u] + 1;
parent[v] = u;
dfs(v, dist, parent, adj);
}
}
}
}
方法三:拓扑排序
思路与算法
由于树的高度由根节点到叶子节点之间的最大距离构成,假设树中距离最长的两个节点为 ( x , y ) (x,y) (x,y),它们之间的距离为 maxdist = d i s t [ x ] [ y ] \text{maxdist}=dist[x][y] maxdist=dist[x][y],假设 x x x 到 y y y 的路径为 x → p 1 → p 2 → ⋯ → p k − 1 → p k → y x\rightarrow p_1\rightarrow p_2\rightarrow\cdots\rightarrow p_{k-1}\rightarrow p_k\rightarrow y x→p1→p2→⋯→pk−1→pk→y,由方法一可知最小树的根节点一定为该路径中的中间节点,我们尝试删除最外层的度为 1 1 1 的节点 x , y x,y x,y 后,则可以知道路径中与 x , y x,y x,y 相邻的节点 p 1 , p k p_1,p_k p1,pk 此时也变为度为 1 1 1 的节点,此时我们再次删除最外层度为 1 1 1 的节点直到剩下根节点为止。
算法步骤如下:
- 首先找到所有度为 1 1 1 的节点压入队列,此时令节点剩余计数 r e m a i n N o d e s = n remainNodes=n remainNodes=n
- 同时将当前 r e m a i n N o d e s remainNodes remainNodes 计数减去出度为 1 1 1 的节点数目,将最外层的度为 1 1 1 的叶子节点取出,并将与之相邻的节点的度减少,重复上述步骤将当前节点中度为 1 1 1 的节点压入队列中;
- 重复上述步骤,直到剩余的节点数组 r e m a i n N o d e s ≤ 2 remainNodes\le2 remainNodes≤2 时,此时剩余的节点即为当前高度最小树的根节点。
代码
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<Integer> ans = new ArrayList<Integer>();
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<Integer>();
}
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<Integer>();
for (int i = 0; i < n; i++) { //先将节点度数为1的节点入队
if (degree[i] == 1) {
queue.offer(i);
}
}
int remainNodes = n; //剩余节点数为n
while (remainNodes > 2) {
int sz = queue.size(); //队列的大小即为节点度数为1的节点个数
remainNodes -= sz;
for (int i = 0; i < sz; i++) {
int curr = queue.poll();
for (int v : adj[curr]) { //删除队列中节点度数为1的节点,其邻接点度数减1
degree[v]--;
if (degree[v] == 1) {
queue.offer(v);
}
}
}
}
while (!queue.isEmpty()) { //剩下的1到2个节点就是答案
ans.add(queue.poll());
}
return ans;
}
}