每日一题做题记录,参考官方和三叶的题解 |
题目要求
思路一:动态规划
遍历整个图,当前遍历节点为 c u r cur cur,从父节点 f a t h e r father father遍历而来,将要遍历子节点 j j j。
要计算以 c u r cur cur为根的树的最小高度,就是找到 c u r cur cur最远的叶子节点的距离,这个过程可以分为上(父节点方向)和下(子节点方向)两个方向,寻找过程通过DFS实现,向上的最长距离记 u p [ c u r ] up[cur] up[cur],向下的最长距离记 d o w n [ c u r ] down[cur] down[cur],那么最小高度即为 m a x ( u p [ c u r ] , d o w n [ c u r ] ) max(up[cur],down[cur]) max(up[cur],down[cur])。
- 【
u
p
up
up】:向上寻找时要分为两个部分,一个是找父节点的父节点方向(祖辈),另一个是寻找父节点的其他子节点方向(兄弟)。
- 祖辈方向的结果为 m a x ( u p [ c u r ] , u p [ f a t h e r ] + 1 ) max(up[cur], up[father] + 1) max(up[cur],up[father]+1),即当前节点的向上最大值,或父节点的向上最大值加上到 c u r cur cur的 1 1 1;
- 兄弟方向的结果需考虑 f a t h e r father father向下最大值是否经过了 c u r cur cur,若未经过则直接更新为 d o w n [ f a t h e r ] + 1 down[father] + 1 down[father]+1,若经过了则要换次长的路径长度加 1 1 1。
- 【 d o w n down down】:向下的寻找直接遍历即可,根据向上兄弟方向寻找时的需求,需分别记录向下最长的路径长度 d o w n M a x downMax downMax和次长的路径长度 d o w n S e c downSec downSec,并维护最长路径所经过的第一个子节点 i d x M a x idxMax idxMax。
在向上进行遍历时,为了避免父节点为空,不用 f a t h e r father father更新 c u r cur cur,而用 c u r cur cur更新 j j j。
Java
class Solution {
int N = 20001, M = N * 2, idx = 0;
int[] head = new int[M], edge = new int[M], next = new int[M];
int[] downMax = new int[N], downSec = new int[N], up = new int[N], idxMax = new int[N];
void add(int a, int b) { //链式前向星
edge[idx] = b;
next[idx] = head[a];
head[a] = idx++;
}
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
Arrays.fill(head, -1);
for(int[] e : edges) { //建图
int a = e[0], b = e[1];
add(a, b);
add(b, a);
}
dfsD(0, -1);
dfsU(0, -1);
List<Integer> res = new ArrayList<>();
int min = n;
for(int i = 0; i < n; i++) {
int cur = Math.max(downMax[i], up[i]);
if(cur < min) {
min = cur;
res.clear();
res.add(i);
}
else if(cur == min)
res.add(i);
}
return res;
}
//下方的最大高度
int dfsD(int cur, int father) {
for(int i = head[cur]; i != -1; i = next[i]) {
int j = edge[i];
if(j == father)
continue;
int sub = dfsD(j, cur) + 1; //子树高度
if(sub > downMax[cur]) { //更新最大
downSec[cur] = downMax[cur];
downMax[cur] = sub;
idxMax[cur] = j;
}
else if(sub > downSec[cur]) //更新次大
downSec[cur] = sub;
}
return downMax[cur];
}
//上方的最大高度
void dfsU(int cur, int father) {
for(int i = head[cur]; i != -1; i = next[i]) {
int j = edge[i];
if(j == father)
continue;
if(idxMax[cur] != j) //最长路径不经过j
up[j] = Math.max(up[j], downMax[cur] + 1);
else //经过j
up[j] = Math.max(up[j], downSec[cur] + 1);
up[j] = Math.max(up[j], up[cur] + 1);
dfsU(j, cur);
}
}
}
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
链式前向星
- 前几天有学习过, 这篇文章 末尾有三叶姐姐更详细的介绍;
- 数组替代邻接表,
head
替代头指针,edge
串连相邻节点,next
替代next指针,因为边有方向后两个数组大小为head
大小二倍。
C++
❓ ❓ ❓
这个地方会报堆栈溢出的错,明明和Java代码没什么区别,怀疑是递归太深或者是建图的问题,不太好解决就先放着了。
class Solution {
private:
const static int N = 20010, M = N * 2;
int idx = 0;
int head[N], edge[M], next[M];
int downMax[N], downSec[N], up[N], idxMax[N];
void add(int a, int b) { //链式前向星
edge[idx] = b;
next[idx] = head[a];
head[a] = idx++;
}
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
memset(head, -1, N);
for(auto& e : edges) { //建图
int a = e[0], b = e[1];
add(a, b);
add(b, a);
}
dfsD(0, -1);
dfsU(0, -1);
vector<int> res;
int min = n;
for(int i = 0; i < n; i++) {
int cur = max(downMax[i], up[i]);
if(cur < min) {
min = cur;
res.clear();
res.push_back(i);
}
else if(cur == min)
res.push_back(i);
}
return res;
}
//下方的最大高度
int dfsD(int cur, int father) {
for(int i = head[cur]; i != -1; i = next[i]) {
int j = edge[i];
if(j == father)
continue;
int sub = dfsD(j, cur) + 1; //子树高度
if(sub > downMax[cur]) { //更新最大
downSec[cur] = downMax[cur];
downMax[cur] = sub;
idxMax[cur] = j;
}
else if(sub > downSec[cur]) //更新次大
downSec[cur] = sub;
}
return downMax[cur];
}
//上方的最大高度
void dfsU(int cur, int father) {
for(int i = head[cur]; i != -1; i = next[i]) {
int j = edge[i];
if(j == father)
continue;
if(idxMax[cur] != j) //最长路径不经过j
up[j] = max(up[j], downMax[cur] + 1);
else //经过j
up[j] = max(up[j], downSec[cur] + 1);
up[j] = max(up[j], up[cur] + 1);
dfsU(j, cur);
}
}
};
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
思路二:拓扑排序+BFS
设树中相距最远的两个节点为 ( x , y ) (x,y) (x,y),它们之间的距离为 d i s t a n c e = d i s [ x ] [ y ] distance = dis[x][y] distance=dis[x][y],那么所求的根节点即为这条路径上中间的节点。由于是距离最远的,二者必然均为叶子节点(度为1),删除 x x x、 y y y,又会出现新的叶子节点,依次删除最终留下最中间的节点,即为所求。
- 将所有度为 1 1 1的节点添加至队列,并记录剩余节点 r e m a i n C n t = n remainCnt=n remainCnt=n;
- 遍历队列依次更新这些节点的父辈节点,并将新的度为 1 1 1的节点添加至队列,并从 r e m a i n C n t remainCnt remainCnt中减去;
- 直至 r e m a i n C n t ≤ 2 remainCnt\le 2 remainCnt≤2,即剩下最中间的一个or两个节点,即为所求。
Java
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
List<Integer> res = new ArrayList<Integer>();
if(n == 1) {
res.add(0);
return res;
}
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[] e : edges) { //建图并统计度
adj[e[0]].add(e[1]);
adj[e[1]].add(e[0]);
degree[e[0]]++;
degree[e[1]]++;
}
Queue<Integer> que = new ArrayDeque<Integer>(); //度为1的节点
for(int i = 0; i < n; i++)
if(degree[i] == 1)
que.offer(i);
int remainCnt = n;
while(remainCnt > 2) {
int len = que.size();
remainCnt -= len;
for(int i = 0; i < len; i++) {
int cur = que.poll();
for(int v : adj[cur]) {
degree[v]--;
if(degree[v] == 1)
que.offer(v);
}
}
}
while(!que.isEmpty())
res.add(que.poll());
return res;
}
}
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
C++
class Solution {
public:
vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
if(n == 1)
return {0};
vector<int> degree(n);
vector<vector<int>> adj(n);
for(auto& e : edges) { //建图并统计度
adj[e[0]].emplace_back(e[1]);
adj[e[1]].emplace_back(e[0]);
degree[e[0]]++;
degree[e[1]]++;
}
queue<int> que; //存放度为1的节点
vector<int> res;
for(int i = 0; i < n; i++)
if(degree[i] == 1)
que.emplace(i);
int remainCnt = n;
while(remainCnt > 2) {
int len = que.size();
remainCnt -= len;
for(int i = 0; i < len; i++) {
int cur = que.front();
que.pop();
for(auto& v : adj[cur]) {
if(--degree[v] == 1)
que.emplace(v);
}
}
}
while(!que.empty()){
res.emplace_back(que.front());
que.pop();
}
return res;
}
};
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( n ) O(n) O(n)
总结
复习了一下链式前向星存图,发现还不能很熟练地无脑写,就又搜着学习了下。思路一向上和向下分别计算的思路很强,似乎是树形DP的模板题,可以多看看背背。
拓扑排序的证明过程叙述比较简略,因为觉得还挺好理解的。
欢迎指正与讨论! |