「LeetCode」拓扑排序系列(二)

290 阅读3分钟

这是我参与11月更文挑战的第2天,活动详情查看2021最后一次更文挑战

前言

在拓扑排序(一)中我们介绍了拓扑排序问题的场景和两种基本解法,介绍了两个LeetCode上拓扑排序的基本习题,并总结了编写拓扑排序解法代码的一般思路(模板)。由于拓扑排序问题相对其他问题(比如动态规划和贪心)来说更加模板化,其变种只需要在基础写法上增加一些变量和判断流程等即可。下面我们来看一下LeetCode上拓扑排序问题的变种。

变种题

2050. 并行课程 III

介绍

这道题是前不久一道周赛的最后一题,是周赛中难度较低的一道。问题与207. 课程表210. 课程表 II非常类似,很容易就想到用拓扑排序来解决问题。与上述两道题不同的是每门课程需要花一定月份数的时间完成,求的是总的最少完成月份数。

解法思路

用一个数组存储每门课最早的完成时间,在Kahn算法每次拆边时判断这个最早的完成时间是否需要更新,然后最后选出数组的最大值即是所有课程所需要的最少月份数。

题解

class Solution:
    def minimumTime(self, n: int, relations: List[List[int]], time: List[int]) -> int:
        latestTime = [time[i] for i in range(0, n)]
        if len(relations) == 0:
            return max(time)
        adj = [[] for _ in range(n)]
        relationCount = len(relations)
        indegree = [0] * n
        for relation in relations:
            adj[relation[0]-1].append(relation[1]-1)
            indegree[relation[1]-1] += 1
        queue = []
        for i in range(n):
            if indegree[i] == 0:
                queue.append(i)
        while len(queue):
            front = queue.pop(0)
            for tail in adj[front]:
                latestTime[tail] = max(latestTime[tail], latestTime[front] + time[tail])
                indegree[tail] -= 1
                relationCount -= 1
                if indegree[tail] == 0:
                    queue.append(tail)
        return max(latestTime)

310. 最小高度树

介绍

这道题的难点在于将其与拓扑排序联系起来,如果不看LeetCode的Tag的话,很难首先从拓扑排序着手开始做题。总之,这道题要求的是最小高度树的根节点的集合,我们可以抽象成求最内部的一些节点,它们到其他任意节点的距离有一个公共的最小值。

解法思路

可以证明这个公共的最小值必定在目标节点与某个边缘节点之间产生,即这个最小值为点到边缘的距离。使用拓扑排序可以在每一轮将所有“距离边缘距离为1”的点去掉,并把其他所有点到边缘的距离减1,以此我们可以求出所有点到边缘的距离。

一个剪枝的地方是在元素个数只剩下1或者2个的时候跳出循环,剩下的1或者2个元素必定为目标结果。

题解

由于笔者当时用的是C++完成的,所以先提供C++版本,后续提供Python等其他语言的版本

class Solution {
public:
    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
        if(n == 1) return {0};
        if(n == 2) return {0,1};
        vector<int> indegree(n, 0);
        vector<vector<int>> adjList(n);
        for (auto& edge: edges) {
            int a = edge[0],b = edge[1];
            adjList[a].push_back(b);
            adjList[b].push_back(a);
            indegree[a]++,indegree[b]++;
        }
        queue<int> q;
        for (int i = 0; i < n; i++) {
            // <= 考虑了只有一个元素没有indegree的情况
            if (indegree[i] == 1) {
                q.push(i);
            }
        }
        // vector<int> ans;
        int remain = n;
        while (!q.empty()) {
            // ans.clear();
            if (remain == 1 || remain == 2) break;
            int size = q.size();
            while (size--) {
                int front = q.front();
                // ans.push_back(front);
                q.pop();
                remain--;
                for (int tail: adjList[front]) {
                    indegree[tail]--;
                    if (indegree[tail] == 1) q.push(tail);
                }
            }
        }
        // return ans;
        vector<int> res;
        while(!q.empty()){
            res.push_back(q.front());
            q.pop();
        }
        return res;
    }
};

总结

可以看出,拓扑排序问题的难点不在于代码的编写,而在于从题干中抽象出有向无环图的关系,然后把题目要求的结果转化成拓扑排序算法中流程的一些中间值。后续会挑选更加有深度的拓扑排序问题来分享给大家。