图论:1203. 项目管理(以小组为单位进行拓扑排序)

文章目录

LeetCode:1203. 项目管理


建议直接看思路整理

1.问题分析

仔细读题可以发现,如果不考虑小组项目彼此相邻,则项目之间的依赖关系就是一个拓扑排序。

但是如果要考虑小组项目彼此相邻,问题就复杂起来了。

容易想到两种策略,第一种就是前期处理,处理拓扑图使得小组也成为一个约束条件,保证一个小组一个小组输出拓扑序;第二种是后期处理,先求出拓扑序然后按小组进行分类,然后再判断该拓扑序是否合法(该方法复杂度过高)。

我们来看第一种策略,很容易想到将小组也作为一个顶点,其后继是所有它管理的项目,这样轮到该小组,该小组的项目才能进行。而然要使得按一个小组一个小组来,那么前一个小组在输出拓扑序未输出完时,后一个小组不能输出,相当于前一个小组制约了后一个小组。这样的话怎么做呢?

我们知道仅仅加一个小组是不行的,因为其入度为0,导致顺序又是乱的,那我们是否可以用前面一个小组的每个项目都连接到后一个小组,这样的话前面的小组的每一个项目完成了才能进行后面的小组,后面这个小组才能开放自己的项目。如果有多个小组可以进行,那么它们一定互不影响,因为根据前面的约束条件,已经没有其他小组的项目可以约束它,我们将其依次放入一个栈中,以便于之后一个一个开放。并且只能在没有可进行的时候开放,原因在于,可能有的没有小组约束,它们进行完成之后,再考虑小组的,不然也会产生顺序问题。

考虑了这个问题后,一个小组的前驱已经被解决了,但是小组的后继呢? 这你可能会觉得有点矛盾,但并不是这样,小组的约束解决了。但是没有小组的项目也是需要约束的,因为它相当于自己作为自己的小组。

实现如下:时间复杂度: O ( n + m ) O(n+m) O(n+m)

cpp 复制代码
class Solution {
public:
    vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) {
        vector<int> result;//存储最终结果
        stack<int> sta; // 存储可开放的小组
        queue<int> q;
        //预处理,将小组进行约束
        vector<vector<int>> edges(n + m); //edges[i]表示其后继
        vector<int> inDegrees(n + m, 0);//入度表示约束条件,小组的编号增加偏移m

        //求入度 并构造 边集
        for(int i = 0; i < beforeItems.size(); ++ i){
            vector<int> & pre = beforeItems[i];
            //pre 是 i的前驱,pre的小组也必然是i小组的前驱(除非相同)
            for(auto & p : pre){
                edges[p].emplace_back(i);
                inDegrees[i] ++;
                if(group[i] != - 1 && group[p] != group[i]){//任何对项目的约束,也都转化成小组之间的约束
                    if(group[p] != -1){
                        edges[group[p] + n].emplace_back(group[i] + n);
                        inDegrees[group[i] + n] ++;
                    }
                }
            }
        }
        //建立小组对项目的约束
        for(int i = 0; i < group.size(); ++ i){
            if(group[i] != -1){
                edges[group[i] + n].emplace_back(i);
                inDegrees[i] ++;
            }
        }

        //求入度为0
        for(int i = 0; i < inDegrees.size(); ++ i){
            if(inDegrees[i] == 0){
                if(i < n)
                    q.push(i);
                else sta.push(i);
            }
        }

        //构建组内成员对后继项目的共同约束
        unordered_set<int> st;
        for(int i = 0; i < beforeItems.size(); ++ i){
            for(auto & pre : beforeItems[i]){
                if(group[i] == -1 && group[pre] != -1 && st.count(group[pre]) == 0){
                    st.insert(group[pre]);
                    for(auto & v : edges[group[pre] + n]){
                        edges[v].emplace_back(i);
                        inDegrees[i] ++;
                    }
                }
            }
        }

        while(!sta.empty() || !q.empty()){
            if(q.empty()){//只能开放小组了
                q.push(sta.top()); sta.pop();
            }
            while(!q.empty()){
                int u = q.front();q.pop();
                if(u < n) result.emplace_back(u);
                for(auto & v : edges[u]){
                    inDegrees[v] --;
                    if(inDegrees[v] == 0){
                        if(v < n) {q.push(v);}
                        else sta.push(v);
                    }
                }
            }
        }
        if(result.size() != n) return vector<int>{};
        return result;
    }
};

如果将单个项目也当成小组,我们可以进一步优化,对于可以执行的单个项目,约束它的所有项目都已经执行完毕,我们在任何时候执行都可以,由于它也当做小组,为了避免它和可以执行的其他小组冲突,我们将其也入栈,直到无可奈何才执行它。这样就不用创建对于它的约束了。

cpp 复制代码
class Solution {
public:
    vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) {
        vector<int> result;//存储最终结果
        stack<int> sta; // 存储可开放的小组
        queue<int> q;
        //预处理,将小组进行约束
        vector<vector<int>> edges(n + m); //edges[i]表示其后继
        vector<int> inDegrees(n + m, 0);//入度表示约束条件,小组的编号增加偏移m

        //求入度 并构造 边集
        for(int i = 0; i < beforeItems.size(); ++ i){
            vector<int> & pre = beforeItems[i];
            //pre 是 i的前驱,pre的小组也必然是i小组的前驱(除非相同)
            for(auto & p : pre){
                edges[p].emplace_back(i);
                inDegrees[i] ++;
                if(group[i] != - 1 && group[p] != group[i]){//任何对项目的约束,也都转化成对小组的约束
                    edges[p].emplace_back(group[i] + n);
                    inDegrees[group[i] + n] ++;
                }
            }
        }
        //建立小组对项目的约束
        for(int i = 0; i < group.size(); ++ i){
            if(group[i] != -1){
                edges[group[i] + n].emplace_back(i);
                inDegrees[i] ++;
            }
        }

        //求入度为0
        for(int i = 0; i < inDegrees.size(); ++ i){
            if(inDegrees[i] == 0){
                if(i < n)
                    q.push(i);
                else sta.push(i);
            }
        }

        while(!sta.empty() || !q.empty()){
            if(q.empty()){//只能开放小组了
                q.push(sta.top()); sta.pop();
            }
            while(!q.empty()){
                int u = q.front();q.pop();
                if(u < n) result.emplace_back(u);
                for(auto & v : edges[u]){
                    inDegrees[v] --;
                    if(inDegrees[v] == 0){
                        if(v < n && group[v] != -1) {q.push(v);}
                        else sta.push(v);
                    }
                }
            }
        }
        if(result.size() != n) return vector<int>{};
        return result;
    }
};

2.思路整理

这样一个困难题被思考出来感觉非常的不可思议。

思路的主要内容是:

  • 给小组增加编号,小组也当做图中的顶点,小组可以执行了,整个小组项目才可以执行,这样保证了小组项目的约束一致性。
  • 小组之间存在约束条件,并且对于两个项目 ( u , v ) (u,v) (u,v)而言,它们的小组也存在约束即 u u u的小组要先于 v v v的小组项目执行。
    • 如果不对小组进行约束的话,小组入度为0,小组的增加形同虚设
    • 我们找到了两个项目 ( u , v ) (u,v) (u,v)的约束关系,同时找到了小组的约束关系,前一个小组进行之后,后一个小组才能进行。
  • 我们对小组延迟进行执行,将其单独放入一个中,只有拓扑排序时队列中没有可执行项目时,才从中取出一个小组进行执行,这样保证了是一个一个小组进行的,并且这个小组的顺序是满足小组之间的约束条件的。
  • 对于无小组的项目,我们当做是一个项目的小组即可,并且我们只需要在其能运行时将其入保证其不影响当前组的项目顺序即可,这样它执行时,一定是其他小组完成时,而不会插在其他小组中间执行。

总体来看,这个思路和官解思路没啥区别,实际上也是以小组为单位进行拓扑排序 的一种方式。只不过我是增加一个数据结构 和 增加小组之间约束条件 来保证这一点的。

  • 小组之间的约束条件,加上保证正在执行的小组必须完整的执行完,保证了以小组为单位进行拓扑排序。
cpp 复制代码
        //求入度 并构造 边集
        for(int i = 0; i < beforeItems.size(); ++ i){
            vector<int> & pre = beforeItems[i];
            //pre 是 i的前驱,pre的小组也必然是i小组的前驱(除非相同)
            for(auto & p : pre){
                edges[p].emplace_back(i);
                inDegrees[i] ++;
                if(group[i] != - 1 && group[p] != group[i]){//任何对项目的约束,也都转化成小组之间的约束
                    if(group[p] != -1){
                        edges[group[p] + n].emplace_back(group[i] + n);
                        inDegrees[group[i] + n] ++;
                    }
                }
            }
        }

3.官解思路

官解实际上就是先求出小组之间的拓扑序,然后对组内拓扑排序,然后把没有小组的项目单独成组,最后按照组的拓扑排序依次进行执行。和我的思路本质是一样的。

以小组为单位的拓扑排序是个好题,它将拓扑排序的深度提升到了新高度。关键在于小组约束小组成员,单项目单独成组,小组之间按顺序拓扑排序,小组内具有原子性进行拓扑排序。

相关推荐
逝去的秋风28 分钟前
【代码随想录训练营第42期 Day57打卡 - 图论Part7 - Prim算法与Kruskal算法
算法·图论·prim算法
热爱编程的OP18 小时前
图论篇--代码随想录算法训练营第六十一天打卡| Floyd 算法,A*算法
数据结构·c++·学习·算法·图论
Aurora_th18 小时前
图论三元环(并查集的高级应用)
c++·算法·图论·并查集·观察力·三元环
EllinY1 天前
CF 231 E Cactus 题解(仙人掌图上找环)
c++·笔记·算法·深度优先·图论
人才程序员1 天前
CSP-J 算法基础 图论
开发语言·数据结构·c++·算法·青少年编程·图论·编程比赛
热爱编程的OP1 天前
图论篇--代码随想录算法训练营第五十八天打卡|拓扑排序,dijkstra(朴素版),dijkstra(堆优化版)精讲
数据结构·c++·算法·图论
wh233z2 天前
Codeforces Round 969 (Div. 2) (A~D)
c语言·开发语言·数据结构·c++·算法·图论
那一抹阳光多灿烂2 天前
代码随想录训练营 Day58打卡 图论part08 拓扑排序 dijkstra朴素版 + 堆优化版
数据结构·python·算法·图论
buaichifanqie2 天前
拓扑排序算法
c++·算法·排序算法·图论·拓扑排序
背水2 天前
再次探讨最小生成树Prim算法
算法·图论