[LeetCode] 1203. Sort Items by Groups Respecting Dependencies 項目管理



There are n items each belonging to zero or one of m groups where group[i] is the group that the i-th item belongs to and it's equal to -1 if the i-th item belongs to no group. The items and the groups are zero indexed. A group can have no item belonging to it.

Return a sorted list of the items such that:

  • The items that belong to the same group are next to each other in the sorted list.
  • There are some relations between these items where beforeItems[i] is a list containing all the items that should come before the i-th item in the sorted array (to the left of the i-th item).

Return any solution if there is more than one solution and return an empty list if there is no solution.

Example 1:

Input: n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3,6],[],[],[]]
Output: [6,3,4,1,5,2,0,7]

Example 2:

Input: n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3],[],[4],[]]
Output: []
Explanation: This is the same as example 1 except that 4 needs to be before 6 in the sorted list.

Constraints:

  • 1 <= m <= n <= 3 * 104
  • group.length == beforeItems.length == n
  • -1 <= group[i] <= m - 1
  • 0 <= beforeItems[i].length <= n - 1
  • 0 <= beforeItems[i][j] <= n - 1
  • i != beforeItems[i][j]
  • beforeItems[i] does not contain duplicates elements.

這道題給了n個結點,說是其屬於m個組,即群組序號在 [0, m-1] 之間,但是又說了還存在一些結點不屬於任何的群組,其群組號為 -1,這個需要稍后做些特殊的處理。題目同時還說了可能有的群組中沒有結點,現在讓給所有的結點進行排序,需要滿足一些特定的條件:屬於同一個群組的結點必須排在一起,這里還有一個 beforeItems 數組,表示 beforeItems[i] 中的所有結點必須排在結點i的前面,若無法滿足這些條件,則返回空數組。這是一道有向圖的排序問題,一般都是要使用拓撲排序 Topological Sort 來做,這里比較麻煩的是存在不同的群組,之間可能並不連通,可能需要多次的拓撲排序。不過這里的 beforeItems 數組倒是有可能把不同群組的結點連接起來,這里進行兩次拓撲排序,分別是針對群組層面和結點層面。既然要針對群組層面,就要先處理一下那些不屬於任何群組的結點,因為其群組號為 -1,不方便處理,這里可以從m開始,分別賦一個不同的值,這樣所有的群組號就是正數了。然后就可以新建圖的結構了,這里還是用鄰接鏈表的形式來存有向圖,用 graphGroup 和 graphItem 分別表示群組和結點的圖結構,同時用兩個對應的入度數組 inGroup 和 inItem。接下來就是建立圖結構了,遍歷每個結點i,先找出其屬於哪個群組,這里用 to_group 表示,然后就是遍歷其對應的 beforeItems 中的結點j,因為這些結點必須在結點i之前,所以它們都應該連到i。對於每個結點j,找出其屬於哪個群組,用 from_group 來表示,若此時i和j屬於不同的兩個群組,且之前二者沒有建立聯系,則此時將二者的群組連接起來,並且對應入度 inGroup 自增1,這是更新了群組的圖結構。接下來還要更新結點的圖結構,若二個結點沒有相連,則將j連到i,並且i的入度自增1。

當群組圖和結點圖都建立好了之后,就可以進行拓撲排序了,將排序的過程放到一個子函數中,這樣就可以復用代碼了。先來看如何進行拓撲排序,可以用 BFS 或者 DFS 來遍歷有向圖。這里用 BFS 來做,借助隊列 queue 來遍歷,遍歷入度數組,將所有入度為0的結點都放入 queue 中。進行 while 循環,取出隊首元素t,將其加入結果 res 中,然后遍歷和其所有相連的結點 next,將其對應的入度值減1,若減到0了,則將該結點加入 queue 中。遍歷完成了之后,再次檢查入度數組,若此時還有大於0的入度值,則表示可能出現環,返回空數組,否則就返回結果 res 即可。在分別得到了群組和結點的有序排列 group_sorted 和 item_sorted 之后,先進行判斷,若有任意一個為空,則返回空數組。接下來需要將排序后的結點分組進行保存,用一個二維數組 group2item,遍歷所有有序的 item,將其放到其對應的群組內。等結點按群組歸納好了之后,就可以排最終的結點順序了,需要根據排好的群組順序進行排列,按群組順序取出其中的所有結點加入結果 res 即可,參見代碼如下:


解法一:

class Solution {
public:
    vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) {
        for (int i = 0; i < n; ++i) {
            if (group[i] == -1) group[i] = m++;
        }
        vector<unordered_set<int>> graphGroup(m), graphItem(n);
        vector<int> inGroup(m), inItem(n), res;
        for (int i = 0; i < n; ++i) {
            int to_group = group[i];
            for (int j : beforeItems[i]) {
                int from_group = group[j];
                if (from_group != to_group && !graphGroup[from_group].count(to_group)) {
                    graphGroup[from_group].insert(to_group);
                    ++inGroup[to_group];
                }
                if (!graphItem[j].count(i)) {
                    graphItem[j].insert(i);
                    ++inItem[i];
                }
            }
        }
        vector<int> group_sorted = helper(graphGroup, inGroup), item_sorted = helper(graphItem, inItem);
        if (group_sorted.empty() || item_sorted.empty()) return {};
        vector<vector<int>> group2item(m);
        for (int item : item_sorted) {
            group2item[group[item]].push_back(item);
        }
        for (int group_id : group_sorted) {
            for (int item : group2item[group_id]) {
                res.push_back(item);
            }
        }
        return res;
    }
    vector<int> helper(vector<unordered_set<int>>& g, vector<int>& inDegree) {
        vector<int> res;
        queue<int> q;
        for (int i = 0; i < inDegree.size(); ++i) {
            if (inDegree[i] == 0) q.push(i);
        }
        while (!q.empty()) {
            int t = q.front(); q.pop();
            res.push_back(t);
            for (int next : g[t]) {
                if (--inDegree[next] == 0) {
                    q.push(next);
                } 
            }
        }
        for (int i = 0; i < inDegree.size(); ++i) {
            if (inDegree[i] > 0) return {};
        }
        return res;
    }
};

我們再來看一種只需要一個圖結構的方法,主要參考了 votrubac 大神的帖子,這里需要用一些輔助結點,具體來說,是在所有群組中都加了一個公共的起始結點,和終止結點,所謂的起始結點,就是該結點通向群組內所有的結點,同理,群組內所有的結點都通向終止結點,這樣做的好處是可以有一個公用的入度為0的起始結點開始每個群組的拓撲排序,則群組之間不會互相干擾。由於每個群組都要新增兩個輔助結點,則總共的結點數就變成了 n + 2*m,這樣就只需要一個結點的圖結構就行了。這里還是用鄰接鏈表的形式來保存圖結構,由於沒有查找需求,直接使用一個二維數組即可。每個群組的公用起始結點標號為 n + group[i],終止標號為 n + m + group[i],這樣就互不沖突了。遍歷所有的結點,若該結點的群組號不是 -1,則將起始結點連到該結點i,且將該結點i連到終止結點。然后再遍歷 beforeItems[i] 中的結點j,若結點i和j屬於同一個群組,且均不是 -1,則將結點j連到結點i。否則計算結點i的群組號,若其屬於 -1,則用i,否則用其起始結點 n + group[i],同理,對於結點j進行相同的操作,然后把結點j所在的群組連到結點i所在的群組。

這樣圖結構就建立好了,可以進行拓撲排序了,這里從 n + 2*m - 1 結點開始往前遍歷,因為后面的結點值都是新增的輔助結點,都是群組的起始結點或者終止結點,從這些結點開始拓撲排序,就可以完整保持每個群組內部的結點順序,同時群組之間的順序也可以保持。為了和上面的解法區分開來,這里使用 DFS 來遍歷。這里用一個狀態數組 state,大小為 n + 2*m,有三種狀態,0表示還未遍歷,1表示開始了遍歷,但此時還還在繼續遍歷和其相連的結點中,2表示已經完成了當前結點的遍歷。在遞歸函數中,首先判斷當前結點的狀態,若不為0,則繼續判斷,若為2,則返回 true,否則返回 false(此時表示有環存在,無法拓撲排序)。否則將當前結點狀態改為1,遍歷所有和其相連的結點 next,並對其調用遞歸函數,若有任意一個結點返回 false 了,則直接返回 false。結束之后,將當前結點狀態改為2,並將結點加入結果 res,返回 true 即可。所有的拓撲排序結束后,此時的順序是和要求的順序是相反的,因為在遞歸中是直接將結點加到 res 末尾了,而不是開頭。不過沒關系,這里直接整個翻轉一下就行了,最后別忘了去掉所有的輔助結點,參見代碼如下:


解法二:

class Solution {
public:
    vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) {
        vector<int> t, res(n), state(n + 2 * m);
        vector<vector<int>> g(n + 2 * m);
        for (int i = 0; i < n; ++i) {
            if (group[i] != -1) {
                g[n + group[i]].push_back(i);
                g[i].push_back(n + m + group[i]);
            }
            for (int j : beforeItems[i]) {
                if (group[i] != -1 && group[i] == group[j]) {
                    g[j].push_back(i);
                } else {
                    int p = group[i] == -1 ? i : n + group[i];
                    int q = group[j] == -1 ? j : n + m + group[j];
                    g[q].push_back(p);
                }
            }
        }
        for (int i = (int)g.size() - 1; i >= 0; --i) {
            if (!helper(g, i, state, t)) return {};
        }
        reverse(t.begin(), t.end());
        copy_if(t.begin(), t.end(), res.begin(), [&](int i) {return i < n;});
        return res;
    }
    bool helper(vector<vector<int>>& g, int i, vector<int>& state, vector<int>& res) {
        if (state[i] != 0) return state[i] == 2;
        state[i] = 1;
        for (int next : g[i]) {
            if (!helper(g, next, state, res)) return false;
        }
        state[i] = 2;
        res.push_back(i);
        return true;
    }
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/1203


參考資料:

https://leetcode.com/problems/sort-items-by-groups-respecting-dependencies/

https://leetcode.com/problems/sort-items-by-groups-respecting-dependencies/discuss/402945/C%2B%2B-with-picture-generic-topological-sort

https://leetcode.com/problems/sort-items-by-groups-respecting-dependencies/discuss/1106701/C%2B%2B-Two-level-topological-sort.-Peel-off-the-tricky-parts-then-do-normal-TopoSort.


LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM