题目描述
对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树。给出这样的一个图,写出一个函数找到所有的最小高度树并返回他们的根节点。
格式:
该图包含 n
个节点,标记为 0
到 n - 1
。给定数字 n
和一个无向边 edges
列表(每一个边都是一对标签)。
你可以假设没有重复的边会出现在 edges
中。由于所有的边都是无向边, [0, 1]
和 [1, 0]
是相同的,因此不会同时出现在 edges
里。
示例:
输入: n = 4, edges = [[1, 0], [1, 2], [1, 3]]
0
|
1
/ \
2 3
输出: [1]
输入: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]
0 1 2
\ | /
3
|
4
|
5
输出: [3, 4]
说明:
- 根据树的定义,树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
- 树的高度是指根节点和叶子节点之间最长向下路径上边的数量。
给定代码
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
}
}
题解一
第一眼看到这个题,这不就是Floyd算法能解决的吗。
典型的多源最短路径问题,打完表之后,找到每个节点到任意节点的最长距离,就是树的高度了。
然后再选一个高度最小的即可。
思路很理想,但是超时了!!!
六十多个数据只过了五十多个,被一个800节点的图拦住了, O ( V 3 ) O(V^3) O(V3)可不是吹的。。。
代码如下:
class Solution {
private static final int MAX = 0x3f3f3f3f;
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
if (n == 0) {
return new ArrayList<>();
}
// 构建邻接表
int [][] dist = new int[n][n];
for (int [] di : dist) {
Arrays.fill(di, MAX);
}
for (int i = 0 ; i < n ; i++) {
dist[i][i] = 0;
}
for (int [] edge : edges)
{
dist[edge[0]][edge[1]] = 1;
dist[edge[1]][edge[0]] = 1;
}
// Floyd
for (int redirect = 0 ; redirect < n ; redirect++)
{
for (int i = 0 ; i < n ; i++)
{
for (int j = 0 ; j < n ; j++)
{
if (dist[i][redirect] != MAX && dist[redirect][j] != MAX && dist[i][j] > dist[i][redirect] + dist[redirect][j]) {
dist[i][j] = dist[i][redirect] + dist[redirect][j];
}
}
}
}
List<Integer> result = new ArrayList<>();
// 找出最小高度
int min = MAX;
for (int i = 0 ; i < n ; i++)
{
int tmpMax = -1;
for (int j = 0 ; j < n ; j++)
{
if (dist[i][j] > tmpMax) {
tmpMax = dist[i][j];
}
}
if (tmpMax <= min) {
if (tmpMax < min) {
result.clear();
min = tmpMax;
}
result.add(i);
}
}
return result;
}
}
时间复杂度:
O
(
V
3
)
O(V^3)
O(V3)。
Floyd核心逻辑三重循环,耗时
O
(
V
3
)
O(V^3)
O(V3)。
空间复杂度:
O
(
V
2
)
O(V^2)
O(V2)。
邻接矩阵为二维数组,数组长度为节点数目
V
V
V,所以占空间
O
(
V
2
)
O(V^2)
O(V2)。
题解二
说实话,当时思维固化在这是个多源最短路径问题,就在想Floyd算法怎么会超时,这算法不就是解决这种问题的吗?
其实我把问题复杂化了,其实这是一个类似拓扑排序的解法。
稍微分析一下,发现,越是靠里面的节点越有可能是最小高度树。
我们从边缘开始,先找到所有入度为1的节点,然后把所有入度为1的节点进队列,然后不断地处理相邻节点,最后找到的就是两边同时向中间靠近的节点,那么这个中间节点就相当于把整个距离二分了,那么它当然就是到两边距离最小的点啦,也就是到其他叶子节点最近的节点了。
几个注意点!!!
- 题目说了给定的图是具有树特征的无向图,所以放心不会有孤儿节点。(除了单树根情况)
- 为什么将入度为1的节点入队呢?因为这是有向图,节点只要连通,入度就至少为1。(有向图拓扑排序是入度为0时再进队)
- 本题不能一个一个的删除入度为1的点,而应在一个循环中,一次性删除入度为1的点,一次缩小一圈。
- 什么时候该向结果集里填充数据呢?缩小n圈后,队列里的节点数和图中剩余未访问节点数相同时,说明这是最后一波了,这一波的内容就全是答案。
代码:
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
if (n == 0) {
return new ArrayList<>();
}
// 构建邻接表
List<Integer> [] graph = new List[n];
for (int i = 0 ; i < n ; i++) {
graph[i] = new ArrayList<>();
}
// 创建入度数组
int [] degree = new int[n];
for (int [] edge : edges)
{
graph[edge[0]].add(edge[1]);
graph[edge[1]].add(edge[0]);
degree[edge[0]]++;
degree[edge[1]]++;
}
Deque<Integer> deque = new ArrayDeque<>();
// 将入度为1的节点入队
for (int i = 0 ; i < n ; i++)
{
// 这里本来是 degree[i] == 1 的
// 加小于号处理单树根的情况(只有一个节点,入度就为0啦)
// 其实也可以开局判断直接返回,懒得写...
if (degree[i] <= 1) {
deque.offerFirst(i);
}
}
// 记录未访问节点数
int count = n;
// flag标识是否需要填充结果集
boolean flag = false;
List<Integer> result = new ArrayList<>();
while (deque.size() > 0)
{
int size = deque.size();
// 判断是不是最后一波
flag = size == count;
// 一次操作一批
for (int i = 0 ; i < size ; i++)
{
// 节点出队,count减一
int pop = deque.pollLast();
count--;
// 如果是最后一波,开始向结果集填值
if (flag) {
result.add(pop);
}
// 处理相邻节点
for (int near : graph[pop])
{
// 入度减一
degree[near]--;
// 如果入度为1,进队
if (degree[near] == 1) {
deque.offerFirst(near);
}
}
}
}
return result;
}
}
V V V为节点数, M M M为边数。
时间复杂度:
O
(
V
+
M
)
O(V + M)
O(V+M)。
每个节点进队操作了一次,无向图每条边操作了两次。
空间复杂度:
O
(
V
+
M
)
O(V + M)
O(V+M)
邻接表占空间
V
+
M
V + M
V+M。