文章目录
- 1.问题分析
- 2.思路整理
- 3.官解思路
LeetCode:1203. 项目管理
建议直接看思路整理
1.问题分析
仔细读题可以发现,如果不考虑小组项目彼此相邻,则项目之间的依赖关系就是一个拓扑排序。
但是如果要考虑小组项目彼此相邻,问题就复杂起来了。
容易想到两种策略,第一种就是前期处理,处理拓扑图使得小组也成为一个约束条件,保证一个小组一个小组输出拓扑序;第二种是后期处理,先求出拓扑序然后按小组进行分类,然后再判断该拓扑序是否合法(该方法复杂度过高)。
我们来看第一种策略,很容易想到将小组也作为一个顶点,其后继是所有它管理的项目,这样轮到该小组,该小组的项目才能进行。而然要使得按一个小组一个小组来,那么前一个小组在输出拓扑序未输出完时,后一个小组不能输出,相当于前一个小组制约了后一个小组。这样的话怎么做呢?
我们知道仅仅加一个小组是不行的,因为其入度为0
,导致顺序又是乱的,那我们是否可以用前面一个小组的每个项目都连接到后一个小组,这样的话前面的小组的每一个项目完成了才能进行后面的小组,后面这个小组才能开放自己的项目。如果有多个小组可以进行,那么它们一定互不影响,因为根据前面的约束条件,已经没有其他小组的项目可以约束它,我们将其依次放入一个栈中,以便于之后一个一个开放。并且只能在没有可进行的时候开放,原因在于,可能有的没有小组约束,它们进行完成之后,再考虑小组的,不然也会产生顺序问题。
考虑了这个问题后,一个小组的前驱已经被解决了,但是小组的后继呢? 这你可能会觉得有点矛盾,但并不是这样,小组的约束解决了。但是没有小组的项目也是需要约束的,因为它相当于自己作为自己的小组。
实现如下:时间复杂度:
O
(
n
+
m
)
O(n+m)
O(n+m)
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;
}
};
如果将单个项目也当成小组,我们可以进一步优化,对于可以执行的单个项目,约束它的所有项目都已经执行完毕,我们在任何时候执行都可以,由于它也当做小组,为了避免它和可以执行的其他小组冲突,我们将其也入栈,直到无可奈何才执行它。这样就不用创建对于它的约束了。
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)的约束关系,同时找到了小组的约束关系,前一个小组进行之后,后一个小组才能进行。
- 我们对小组延迟进行执行,将其单独放入一个
栈
中,只有拓扑排序时队列
中没有可执行项目时,才从栈
中取出一个小组进行执行,这样保证了是一个一个小组进行的,并且这个小组的顺序是满足小组之间的约束条件的。 - 对于无小组的项目,我们当做是一个项目的小组即可,并且我们只需要在其能运行时将其入
栈
保证其不影响当前组的项目顺序即可,这样它执行时,一定是其他小组完成时,而不会插在其他小组中间执行。
总体来看,这个思路和官解思路没啥区别,实际上也是以小组为单位进行拓扑排序的一种方式。只不过我是增加一个数据结构栈
和 增加小组之间约束条件 来保证这一点的。
- 小组之间的约束条件,加上
栈
保证正在执行的小组必须完整的执行完,保证了以小组为单位进行拓扑排序。
//求入度 并构造 边集
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.官解思路
官解实际上就是先求出小组之间的拓扑序,然后对组内拓扑排序,然后把没有小组的项目单独成组,最后按照组的拓扑排序依次进行执行。和我的思路本质是一样的。
以小组为单位的拓扑排序是个好题,它将拓扑排序的深度提升到了新高度。关键在于小组约束小组成员,单项目单独成组,小组之间按顺序拓扑排序,小组内具有原子性进行拓扑排序。