题目
For a 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]
方法一:
题目要求找到所有最小高度的树根节点,直观上做法可以是:将[0~n-1]所有节点都模拟作为根节点来计算其构成的树的高度,然后寻找最小高度对应的根节点即可。则根据树的结构,可以得到以下树的高度计算公式:
height(root) = max(height(children1), height(children2), ……, height(childrenM)) + 1
接下来,利用递归即可实现树的高度的求值。
class Solution {
public:
// dfs递归函数:返回以node为根节点的子树的高度
int dfs(int node, int n, vector<pair<int, int>>& edges, bool* searched){ // searched数组标记节点是否访问过
int maxx = 1, temp;
for (auto edge : edges) { // 遍历所有边
// 查找与node相连的另外一个节点
int anotherNode = node;
if (edge.first == node) anotherNode = edge.second;
else if (edge.second == node) anotherNode = edge.first;
// 若该节点尚未被访问
if (!searched[anotherNode]) {
searched[anotherNode] = true;
// 对应于前面分析的树的高度计算公式
temp = dfs(anotherNode, n, edges, searched)+1;
if(maxx < temp) maxx = temp;
}
}
return maxx;
}
// findMinHeightTrees函数:返回最小高度树的根节点的向量
vector<int> findMinHeightTrees(int n, vector<pair<int, int>>& edges){
int heights[n]={0}, minHeight=INT_MAX;
vector<int>minHeightNodes;
// 遍历所有节点,将所有节点都作为根节点求树的高度
for(int i=0;i<n;i++){
bool searched[n]={0};
searched[i]=true;
heights[i] = dfs(i, n, edges, searched);
if(minHeight > heights[i]){
minHeight = heights[i];
}
}
// 找出最小高度树对应的根节点
for(int i=0;i<n;i++){
if(minHeight == heights[i])
minHeightNodes.push_back(i);
}
return minHeightNodes;
}
};
以上算法的时间复杂度为
O
(
V
∗
E
)
O(V*E)
O(V∗E)(V表示节点数量,E表示边数量),时间复杂度过高,导致submit
不通过。
利用map数据结构,来存储每个节点对应连接的其他节点所组成向量的一个映射map<int, vector<int>>
,(类似于邻接表)。相对于上面代码做了一些剪枝和优化。
class Solution {
public:
int dfs(int node, int n, unordered_map<int, vector<int>>& graph, bool* searched){
int maxx = 1, temp;
for (auto anotherNode : graph[node]) {
if (!searched[anotherNode]) {
searched[anotherNode] = true;
temp = dfs(anotherNode, n, graph, searched)+1;
if(maxx < temp) maxx = temp;
}
}
return maxx;
}
vector<int> findMinHeightTrees(int n, vector<pair<int, int>>& edges){
int heights[n]={0}, minHeight=INT_MAX;
vector<int>minHeightNodes;
unordered_map<int, vector<int>> graph;
for(auto edge : edges) {
graph[edge.first].push_back(edge.second);
graph[edge.second].push_back(edge.first);
}
for(int i=0;i<n;i++){
bool searched[n]={0};
searched[i]=true;
heights[i] = dfs(i, n, graph, searched);
if(minHeight > heights[i]){
minHeight = heights[i];
}
}
for(int i=0;i<n;i++){
if(minHeight == heights[i])
minHeightNodes.push_back(i);
}
return minHeightNodes;
}
};
此时,尽管效率有所提升:
当数据量过大时,提交仍然无法通过:
方法二:
根据题意可以知道,返回的节点的数量必定为1或者2。否则的话,假如节点数量为3,因为输入数据保证该图具有树的特征,所以这三个点必定相连,则这三个点中必定存在一个节点的高度比其他两个节点的高度要大,所以返回的节点数量不可能大于2。
此时,可以采用剥洋葱的思想:
第一步,将所有度为0或1的节点加入到队列中(洋葱最外层的皮);
第二步,弹出队首的节点,并将与队首节点相连的节点的度减少1,此时,将度为0或者1的节点加入到队列中(不断将洋葱外围的皮剥掉);
第三部,直到所有节点都被加入到队列一次,且当前队列只剩下2个或者1个节点时,返回该队列中的节点即可(返回洋葱最中心的部分)。
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<pair<int, int>>& edges){
queue<int>nodes;
int degree[n] = {0};
bool searched[n]={0}; // searched保证了已访问的节点不会再次被访问
unordered_map<int, vector<int>> graph;
// 统计所有节点的度、预处理所有边得到映射
for(auto edge : edges) {
degree[edge.first]++;
degree[edge.second]++;
graph[edge.first].push_back(edge.second);
graph[edge.second].push_back(edge.first);
}
// 将所有度等于0/1的所有节点加入到队列中
// 问:由于题目保证输入数据满足树的特征,为什么会存在度数为0的节点?
// 答:若输入数据为1 [],此时,该节点度数即为0
for(int i=0;i<n;i++){
if(degree[i] ==1 || degree[i] == 0){
searched[i] = true;
nodes.push(i);
}
}
// 最外层while循环判断队列是否为空
vector<int>roots;
while(nodes.size()){
int size = nodes.size();
roots.clear(); //每次将roots数组清空,只有最中心的洋葱才会保留
// 遍历每次最外围的所有节点(相当于洋葱最外圈)
while(size>0){
int node = nodes.front();
nodes.pop();
size--;
roots.push_back(node);
// 此循环,将与外部节点相连的,且度数减1后等于0/1的内部节点加入到队列中
for(auto anotherNode : graph[node]){
if(!searched[anotherNode]){
if(--degree[anotherNode] == 0 || degree[anotherNode] == 1){
searched[anotherNode] = true;
nodes.push(anotherNode);
}
}
}
}
}
return roots;
}
};
此算法可以通过所有测试数据。