LeetCode-310. 最小高度树【dfs,bfs,拓扑排序】

题目描述:

树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。

给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树

请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。

树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。在这里插入图片描述

解题思路一:拓扑排序(无向图)

结论:逐层删除叶子节点和边,剩下的最后小于等于2个节点即是根节点
1.首先将所有叶子节点压入队列,此时令节点剩余计数 remainNodes=n;
2.同时将当前 remainNodes 计数减去叶子节点数目,将当前队列中叶子节点取出,并将与之相邻的节点的度减少1,重复上述步骤将当前节点中度为 1 的节点压入队列中;
3.重复上述步骤,直到剩余的节点数组 remainNodes≤2 时,此时剩余的节点即为当前高度最小树的根节点。
emplace=push但是二者有一些区别。emplace要新和高效一些。

class Solution {
public:
	//拓扑排序实现要点:1.记录当前节点的相邻节点。2.每个节点的度。3.队列实现(度为1入队)
    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
        if(n==1){//如果无向图只有一个节点可直接返回{0};
            return {0};//返回值类型是vector<int>
        }
        vector<int> degree(n);
        vector<vector<int>> adj(n);//n个节点
        for(auto &edge: edges){//要拓扑排序必须找到每个节点相邻的节点和每个节点的度
            adj[edge[0]].emplace_back(edge[1]);
            adj[edge[1]].emplace_back(edge[0]);
            ++degree[edge[0]];
            ++degree[edge[1]];
        }
        queue<int> qu;//队列实现拓扑排序
        vector<int> ans;//记录最终根节点
        for(int i=0;i<n;++i){//队列初始化
            if(degree[i]==1){//叶子节点入队
                qu.emplace(i);
            }
        }
        int remainNodes=n;
        while(remainNodes>2){
            int sz=qu.size();
            remainNodes-=sz;//减去叶子节点数
            for(int i=0;i<sz;++i){
                int curr=qu.front();//取队首节点
                qu.pop();
                for(auto &v: adj[curr]){//取当前节点的相邻节点
                    if(--degree[v]==1){//节点v度先减1若减去之后v的度为1则入队.
                        qu.emplace(v);
                    }
                }
            }
        }
        while(!qu.empty()){
            ans.emplace_back(qu.front());
            qu.pop();
        }
        return ans;
    }
};

在这里插入图片描述
emplace操作是C++11新特性,新引入的的三个成员emplace_front、emplace 和 emplace_back。这些操作构造而不是拷贝元素到容器中,这些操作分别对应push_front、insert 和push_back,允许我们将元素放在容器头部、一个指定的位置和容器尾部。

两者的区别 :
当调用insert时,是将对象传递给insert,对象被拷贝到容器中,而当我们使用emplace时,是将参数传递给构造函,emplace使用这些参数在容器管理的内存空间中直接构造元素。
复杂度分析
时间复杂度:O(n),其中 n 是为节点的个数。图中边的个数为 n−1,因此建立图的关系需要的时间复杂度为 O(n),通过广度优先搜索需要的时间复杂度为 O(n+n−1),求最长路径的时间复杂度为 O(n),因此总的时间复杂度为 O(n)。

空间复杂度:O(n),其中 n 是节点的个数。由于题目给定的图中任何两个顶点都只有一条路径连接,因此图中边的数目刚好等于 n−1,用邻接表构造图所需的空间刚好为 O(2×n),存储每个节点的距离和父节点均为 O(n),使用广度优先搜索时,队列中最多有 n 个元素,所需的空间也为 O(n),因此空间复杂度为 O(n)。

解题思路二:广度优先搜索BFS(看注释)

结论:找最长的一条路径
1.从节点0开始找最远节点x
2.从节点x开始找最远节点y
3.x-y即是最远路径

class Solution {
public:
	//BFS算法要点:1.记录当前节点的相邻节点。2.访问数组(每个节点仅访问一次)3.队列实现(未访问入队)
    int findLongestNode(int u, vector<int> & parent, vector<vector<int>>& adj) {
    //parent记录从u节点开始每个节点的父节点,BFS算法
        int n = adj.size();
        queue<int> qu;
        vector<bool> visit(n);//访问数组。
        qu.emplace(u);
        visit[u] = true;
        int node = -1;
  
        while (!qu.empty()) {
            int curr = qu.front();
            qu.pop();
            node = curr;//记录队列中广度搜索最后一个节点,即是距离节点u最远的节点。
            for (auto & v : adj[curr]) {//每次广度搜索一层
                if (!visit[v]) {
                    visit[v] = true;
                    parent[v] = curr;
                    qu.emplace(v);
                }
            }
        }
        return node;
    }
    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
        if (n == 1) {
            return {0};
        }
        vector<vector<int>> adj(n);
        for (auto & edge : edges) {
            adj[edge[0]].emplace_back(edge[1]);
            adj[edge[1]].emplace_back(edge[0]);
        }
        vector<int> parent(n, -1);
        /* 找到与节点 0 最远的节点 x */
        int x = findLongestNode(0, parent, adj);
        /* 找到与节点 x 最远的节点 y */
        int y = findLongestNode(x, parent, adj);
        /* 求出节点 x 到节点 y 的路径 */
        vector<int> path;//记录一条从x到y的路径
        parent[x] = -1;//parent此时记录的是x开始广度搜索之后每个节点的父节点,可想而知x是父节y是最后一个子节点,即一条从x到y的路径。
        while (y != -1) {
            path.emplace_back(y);
            y = parent[y];
        }
        int m = path.size();
        if (m % 2 == 0) {//如果m为偶数,此时最小高度树的根节点为{path[m / 2 - 1], path[m / 2]}
            return {path[m / 2 - 1], path[m / 2]};
        } else {//如果m为奇数,此时最小高度树的根节点为 {path[m / 2]}
            return {path[m / 2]};
        }//这是结论。证明可以看链接:https://leetcode-cn.com/problems/minimum-height-trees/solution/zui-xiao-gao-du-shu-by-leetcode-solution-6v6f/
    }
};

解题思路三:深度优先搜索DFS(递归传送的参数可以稍微注意一下)

和广度优先搜索差不多

class Solution {
public:
	//DFS算法要点:1.记录当前节点的相邻节点。2.递归实现(若dist不为-1则深度搜索一遍。)即记录每个节点到当前节点的深度。
    void dfs(int u, vector<int> & dist, vector<int> & parent, const vector<vector<int>> & adj) {
        for (auto & v : adj[u]) {
            if (dist[v] < 0) {
                dist[v] = dist[u] + 1;
                parent[v] = u;
                dfs(v, dist, parent, adj); 
            }
        }
    }

    int findLongestNode(int u, vector<int> & parent, const vector<vector<int>> & adj) {
        int n = adj.size();
        vector<int> dist(n, -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;
    }

    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
        if (n == 1) {
            return {0};
        }
        vector<vector<int>> adj(n);
        for (auto & edge : edges) {
            adj[edge[0]].emplace_back(edge[1]);
            adj[edge[1]].emplace_back(edge[0]);
        }
        vector<int> parent(n, -1);
        /* 找到距离节点 0 最远的节点  x */
        int x = findLongestNode(0, parent, adj);
        /* 找到距离节点 x 最远的节点  y */
        int y = findLongestNode(x, parent, adj);
        /* 找到节点 x 到节点 y 的路径 */
        vector<int> path;
        parent[x] = -1;
        while (y != -1) {
            path.emplace_back(y);
            y = parent[y];
        }
        int m = path.size();
        if (m % 2 == 0) {
            return {path[m / 2 - 1], path[m / 2]};
        } else {
            return {path[m / 2]};
        }
    }
};
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法。 LeetCode的动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旋转的油纸伞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值