Java&C++题解与拓展——leetcode310.最小高度树【复习链式前向星】

547 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

每日一题做题记录,参考官方和三叶的题解

题目要求

在这里插入图片描述

思路一:动态规划

遍历整个图,当前遍历节点为curcur,从父节点fatherfather遍历而来,将要遍历子节点jj

要计算以curcur为根的树的最小高度,就是找到curcur最远的叶子节点的距离,这个过程可以分为上(父节点方向)和下(子节点方向)两个方向,寻找过程通过DFS实现,向上的最长距离记up[cur]up[cur],向下的最长距离记down[cur]down[cur],那么最小高度即为max(up[cur],down[cur])max(up[cur],down[cur])

  • upup】:向上寻找时要分为两个部分,一个是找父节点的父节点方向(祖辈),另一个是寻找父节点的其他子节点方向(兄弟)。
    • 祖辈方向的结果为max(up[cur],up[father]+1)max(up[cur], up[father] + 1),即当前节点的向上最大值,或父节点的向上最大值加上到curcur11
    • 兄弟方向的结果需考虑fatherfather向下最大值是否经过了curcur,若未经过则直接更新为down[father]+1down[father] + 1,若经过了则要换次长的路径长度加11
  • downdown】:向下的寻找直接遍历即可,根据向上兄弟方向寻找时的需求,需分别记录向下最长的路径长度downMaxdownMax和次长的路径长度downSecdownSec,并维护最长路径所经过的第一个子节点idxMaxidxMax

在向上进行遍历时,为了避免父节点为空,不用fatherfather更新curcur,而用curcur更新jj

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)

链式前向星

  • 前几天有学习过, 这篇文章 末尾有三叶姐姐更详细的介绍;
  • 数组替代邻接表,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)

思路二:拓扑排序+BFS

设树中相距最远的两个节点为(x,y)(x,y),它们之间的距离为distance=dis[x][y]distance = dis[x][y],那么所求的根节点即为这条路径上中间的节点。由于是距离最远的,二者必然均为叶子节点(度为1),删除xxyy,又会出现新的叶子节点,依次删除最终留下最中间的节点,即为所求。

  • 将所有度为11的节点添加至队列,并记录剩余节点remainCnt=nremainCnt=n
  • 遍历队列依次更新这些节点的父辈节点,并将新的度为11的节点添加至队列,并从remainCntremainCnt中减去;
  • 直至remainCnt2remainCnt\le 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)

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)

总结

复习了一下链式前向星存图,发现还不能很熟练地无脑写,就又搜着学习了下。思路一向上和向下分别计算的思路很强,似乎是树形DP的模板题,可以多看看背背。

拓扑排序的证明过程叙述比较简略,因为觉得还挺好理解的。


欢迎指正与讨论!