【2603. 收集树中金币】

来源:力扣(LeetCode)

描述:

给你一个 n 个节点的无向无根树,节点编号从 0n - 1 。给你整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ai, bi] 表示树中节点 aibi 之间有一条边。再给你一个长度为 n 的数组 coins ,其中 coins[i] 可能为 0 也可能为 11 表示节点 i 处有一个金币。

一开始,你需要选择树中任意一个节点出发。你可以执行下述操作任意次:

  • 收集距离当前节点距离为 2 以内的所有金币,或者
  • 移动到树中一个相邻节点。

你需要收集树中所有的金币,并且回到出发节点,请你返回最少经过的边数。

如果你多次经过一条边,每一次经过都会给答案加一。

示例 1:

【2603. 收集树中金币】_第1张图片

输入:coins = [1,0,0,0,0,1], edges = [[0,1],[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:从节点 2 出发,收集节点 0 处的金币,移动到节点 3 ,收集节点 5 处的金币,然后移动回节点 2

示例 2:

【2603. 收集树中金币】_第2张图片

输入:coins = [0,0,0,1,1,0,0,1], edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[5,6],[5,7]]
输出:2
解释:从节点 0 出发,收集节点 43 处的金币,移动到节点 2 处,收集节点 7 处的金币,移动回节点 0

提示:

  • n == coins.length
  • 1 <= n <= 3 * 104
  • 0 <= coins[i] <= 1
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • ai != bi
  • edges 表示一棵合法的树。

方法:两次拓扑排序

思路与算法

对于给定无根树中度数为 111 的节点,我们称其为「叶节点」。可以发现,对于每一个「叶节点」l,如果 l 上没有金币,那么我们就没有必要走到 l。这是因为对于 l 唯一相邻的那个节点 l′ ,它可以收集到在 l 可以收集到的所有金币,那么:

  • 如果我们从 l 开始出发,那么可以改成从 l′ 开始出发,经过的边数一定减少;

  • 如果我们不从 l 开始出发,那么到达 l′ 之后不必再走向 l,经过的边数一定减少。

因此,我们可以不断地移除给定无根树中没有金币的「叶节点」。当某个「叶节点」被移除后,它唯一相邻的那条边也需要被移除,这样可能会有新的节点变为「叶节点」。我们不断迭代地重复这个过程,直到所有的「叶节点」上都有金币为止。

这一步可以使用基于广度优先搜索的拓扑排序解决。我们首先将所有「叶节点」加入队列中,随后不断从队列中取出节点,将它标记为删除,并判断其唯一相邻的节点是否变为「叶节点」。如果是,就将相邻的节点也加入队列中。

当所有的「叶节点」上都有金币时,我们应该如何解决给定的问题呢?我们可以先思考,如果操作变为「收集距离当前节点距离为 0 以内的所有金币」该如何解决。当距离为 0 时,我们必须要走到对应的节点上才能收集金币,而每个「叶节点」上都有金币,因此我们必须遍历到所有的「叶节点」,这也意味着我们遍历了整颗树。从任一节点出发,遍历整颗树并返回原节点,会经过树上的每条边一次,而树的边数等于点数减一,因此答案为 2(n′ − 1) ,其中 n′ 时经过上文移除后,无根树中节点的数量。

如果操作变为「收集距离当前节点距离为 1 以内的所有金币」呢?我们可以进一步思考得到:当我们即将遍历到「叶节点」时,可以直接返回,因为此时我们与「叶节点」的距离为 1,可以直接收集到金币,不需要走到「叶节点」。因此,我们遍历的范围,就是将树中所有叶节点以及它们唯一相邻的那条边移除后的新树。在新树中的金币可以通过遍历获得到,而不在新树中的金币会在遍历到与其距离为 1 的某个节点时获取到。

因此,当操作变为「收集距离当前节点距离为 2 以内的所有金币」时,我们的方法仍然是类似的,我们只需要将新树中所有的「叶节点」以及它们唯一相邻的那条边移除,得到的新新树就是需要遍历的范围。

这一步同样可以使用基于广度优先搜索的拓扑排序解决。我们进行 222 次如下的操作:首先将所有「叶节点」加入初始队列中,随后不断从初始队列中取出节点,将它标记为删除。

细节

如果 新新树 中没有任何节点,说明在初始的无根树中,存在一个节点可以直接获取到所有金币,答案为 0,否则答案为 2× ( 新新树 中的节点数量 −1)。

代码:

class Solution {
public:
    int collectTheCoins(vector<int>& coins, vector<vector<int>>& edges) {
        int n = coins.size();
        vector<vector<int>> g(n);
        vector<int> degree(n);
        for (const auto& edge: edges) {
            int x = edge[0], y = edge[1];
            g[x].push_back(y);
            g[y].push_back(x);
            ++degree[x];
            ++degree[y];
        }

        int rest = n;
        {
            /* 删除树中所有无金币的叶子节点,直到树中所有的叶子节点都是含有金币的 */
            queue<int> q;
            for (int i = 0; i < n; ++i) {
                if (degree[i] == 1 && !coins[i]) {
                    q.push(i);
                }
            }
            while (!q.empty()) {
                int u = q.front();
                --degree[u];
                q.pop();
                --rest;
                for (int v: g[u]) {
                    --degree[v];
                    if (degree[v] == 1 && !coins[v]) {
                        q.push(v);
                    }
                }
            }
        }
        {
            /* 删除树中所有的叶子节点, 连续删除2次 */
            for (int _ = 0; _ < 2; ++_) {
                queue<int> q;
                for (int i = 0; i < n; ++i) {
                    if (degree[i] == 1) {
                        q.push(i);
                    }
                }
                while (!q.empty()) {
                    int u = q.front();
                    --degree[u];
                    q.pop();
                    --rest;
                    for (int v: g[u]) {
                        --degree[v];
                    }
                }
            }
        }

        return rest == 0 ? 0 : (rest - 1) * 2;
    }
};

时间 764ms 击败 32.86%使用 C++ 的用户
内存 216.27MB击败 50.00%使用 C++ 的用户
复杂度分析

  • 时间复杂度: O(n)。构造图的邻接表,度数数组以及拓扑排序都需要 )O(n) 的时间。
  • 空间复杂度: O(n)。即为图的邻接表,度数数组以及拓扑排序中的队列需要使用的空间。
    author:力扣官方题解

你可能感兴趣的:(LeetCode,数据结构,leetcode,c++)