leetcode 周賽155 | 項目管理之拓撲排序演算法
- 2019 年 10 月 6 日
- 筆記
今天看了下leetcode周賽的題目,沒怎麼做,第四題是一道Hard題目,看了下題意感覺要用拓撲排序,但是這道題除了依賴關係,還有一個分組的變數,導致這道題處理起來會複雜些,可能需要2次拓撲排序或者1次複雜的拓撲排序。
下午看了下排名第一的題解,果然是2次拓撲排序,程式碼比較精簡,值得學習一下。
來看下本次比賽這道題的通過次數
,這次是真的很低。

來看題:
5200. 項目管理 顯示英文描述
公司共有 n 個項目和 m 個小組,每個項目要不沒有歸屬,要不就由其中的一個小組負責。
我們用 group[i] 代表第 i 個項目所屬的小組,如果這個項目目前無人接手,那麼 group[i] 就等於 -1。(項目和小組都是從零開始編號的)
請你幫忙按要求安排這些項目的進度,並返回排序後的項目列表:
同一小組的項目,排序後在列表中彼此相鄰。 項目之間存在一定的依賴關係,我們用一個列表 beforeItems 來表示,其中 beforeItems[i] 表示在進行第 i 個項目前(位於第 i 個項目左側)應該完成的所有項目。 結果要求:
如果存在多個解決方案,只需要返回其中任意一個即可。
如果沒有合適的解決方案,就請返回一個 空列表。
示例 1:

輸入:n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3,6],[],[],[]] 輸出:[6,3,4,1,5,2,0,7]
示例 2:
輸入:n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3],[],[4],[]] 輸出:[] 解釋:與示例 1 大致相同,但是在排序後的列表中,4 必須放在 6 的前面。
大概思路:
1、首先對於所有的項目,將他們之間的依賴關係連成邊,然後進行拓撲排序,解決掉同一個組內元素先後的問題。
2、排序後,進行分組,按組的依賴關係連成邊,然後進行拓撲排序,解決掉多個組的先後問題。
3、這時候可以將排序後的組合併起來,返回即可。
源程式碼:
const int MAXN = 30010; vector<int> v[MAXN]; int deg[MAXN]; void clear(int n) { for (int i = 0; i < n; ++i) { v[i].clear(); deg[i] = 0; } } void addedge(int x, int y) { v[x].push_back(y); deg[y] ++; } int head, tail, Q[MAXN]; bool topsort(int n) { head = 1; tail = 0; for (int i = 0; i < n; ++i) if (!deg[i]) { Q[++tail] = i; } while (head <= tail) { int x = Q[head++]; for (auto y : v[x]) if (!--deg[y]) Q[++tail] = y; } return tail == n; } class Solution { public: vector<int> sortItems(int n, int m, vector<int>& group, vector<vector<int>>& beforeItems) { clear(n); for (int i = 0; i < n; ++i) { for (auto x : beforeItems[i]) { //元素間的依賴關係 addedge(x, i); } } //top排序進行分組 if (!topsort(n)) return {}; int tmp_m = m; for (int i = 0; i < n; ++i) { if (group[i] == -1) { group[i] = m++; } } vector<vector<int>> v(m); for (int i = 1; i <= n; ++i) v[group[Q[i]]].push_back(Q[i]); clear(m); for (int i = 0; i < n; ++i) { for (auto x : beforeItems[i]) { if (group[x] == group[i]) continue; //組之間的依賴關係 addedge(group[x], group[i]); } } //對組進行top排序 if (!topsort(m)) return {}; vector<int> ret; //分組輸出 for (int i = 1; i <= m; ++i) { for (auto x : v[Q[i]]) ret.push_back(x); } return ret; } }; 再來回顧一下拓撲排序:
一個簡單的問題
有n個任務,用1-n進行表示,任務可能存在依賴關係,(u,v)表示v需要依賴任務u,輸出完成所有任務的序列。
拓撲排序
拓撲排序就是解決這種存在先後關係的問題的演算法。在上面的問題中,可以把每個任務看成圖上的一個節點,對於(u, v)表示 u→v。
比如有3個任務,表示為1,2,3,任務依賴關係為(2,3) (3,1) 那麼就有如下圖所示:

任務序列也就是2,3,1。對於有些圖,序列可能是多個。
在圖論中,由一個有向無環圖DAG的頂點組成的序列,當且僅當滿足下列條件時,稱為該圖的一個拓撲排序
1 每個頂點出現且只出現一次; 2 若A在序列中排在B的前面,則在圖中不存在從B到A的路徑。
卡恩演算法
卡恩於1962年提出的演算法。簡單來說就是,假設L是存放結果的列表,先找到那些入度為零的節點,把這些節點放到L中,因為這些節點沒有任何的父節點。然後把與這些節點相連的邊從圖中去掉,再尋找圖中的入度為零的節點。對於新找到的這些入度為零的節點來說,他們的父節點已經都在L中了,所以也可以放入L。重複上述操作,直到找不到入度為零的節點。如果此時L中的元素個數和節點總數相同,說明排序完成;如果L中的元素個數和節點總數不同,說明原圖中存在環,無法進行拓撲排序。
偽程式碼
bool toposort() { q = new queue(); for (i = 0; i < n; i++) if (in_deg[i] == 0) q.push(i); ans = new vector(); while (!q.empty()) { u = q.pop(); ans.push_back(u); for each edge(u, v) { if (--in_deg[v] == 0) q.push(v); } } if (ans.size() == n) { for (i = 0; i < n; i++) std::cout << ans[i] << std::endl; return true; } else { return false; } } 簡單的理解,遍歷每個節點N,從N開始進行DFS搜索,如果能夠完成搜索,則說明存在拓撲排序。如下程式碼。
// dfs 版本 bool dfs(int u) { c[u] = -1; for (int v = 0; v <= n; v++) if (G[u][v]) { if (c[v] < 0) return false; else if (!c[v]) dfs(v); } c[u] = 1; topo.push_back(u); return true; } bool toposort() { topo.clear(); memset(c, 0, sizeof(c)); for (int u = 0; u <= n; u++) if (!c[u]) if (!dfs(u)) return false; reverse(topo.begin(), topo.end()); return true; }