算法期中考到一題關於拓撲序的題目,覺得很值得一寫。
1.什么是拓撲序?
對一個有向無環圖進行拓撲排序,假如圖中存在一條從頂點A到頂點B的路徑,則拓撲序中頂點A出現在頂點B的前面。要注意的是,這是對有向無環圖而言的,假如圖是有環的,拓撲序就無從談起了。在這道題目中,已經假定了圖是一個無環圖。因此不需要進行檢查。
2.怎么得出拓撲序?
有兩種方法,分別基於BFS和DFS,時間復雜度都是O(|V| + |E|)。以這道題作為例子分別說一下:
(1)BFS
這是我最先想到的方法。我們需要:一個數組,統計每個頂點的入度數;一個隊列用於bfs。首先遍歷一次所有的邊,將每個頂點的入度數都求出來,而入度數為0的頂點說明沒有其他頂點指向它。因此先把入度數為0的頂點放進隊列中。
接着用一個循環每次從隊頭取出front,把它放進返回結果的列表中。然后遍歷一遍所有的邊,假如當前邊對應的起始頂點為front,則將邊對應的終結頂點的入度數減1(我們把front放進結果中,也視作把front從圖中去掉了,那入度數自然要減1了)。如果一個頂點的入度數變為0,說明已經沒有其他頂點指向它了,就可以把它放入隊尾。一直到隊列為空時,說明拓撲序已經得到了。
代碼如下:
class Solution { public: vector<int> topologicalSort(int n, vector<pair<int, int> >& edges) { vector<int> res; int * in_degree = new int[n]; queue<int> q; for (int i = 0; i < n; i++) { in_degree[i] = 0; } for (int i = 0; i < edges.size(); i++) { in_degree[edges[i].second]++; } for (int i = 0; i < n; i++) { if (in_degree[i] == 0) { q.push(i); } } while (!q.empty()) { int front = q.front(); q.pop(); res.push_back(front); for (int i = 0; i < edges.size(); i++) { if (edges[i].first == front) { in_degree[edges[i].second]--; if (in_degree[edges[i].second] == 0) { q.push(edges[i].second); } } } } return res; } };
(2)DFS
據說這是神書《算法導論》中提到的算法:用深度搜索來遍歷整個圖,采用一個數組來保存每個頂點完成的時間,這樣這個數組就存放了按先后順序訪問完成的頂點了。然后我們按照頂點訪問的完成時間從大到小排序,得到的就是一個拓撲序了,具體證明如下(來自其他博客):
在這道題中,肯定不用真的開一個數組啊,代碼如下:
class Solution { public: vector<int> topologicalSort(int n, vector<pair<int, int> >& edges) { vector<int> res; stack<int> s; int * isVisited = new int[n]; for (int i = 0; i < n; i++) { isVisited[i] = 0; } for (int i = 0; i < n; i++) { if (!isVisited[i]) dfs(edges, s, isVisited, i); } while (!s.empty()) { res.push_back(s.top()); s.pop(); } return res; } void dfs(vector<pair<int, int> >& edges, stack<int> & s, int * isVisited, int u) { isVisited[u] = 1; for (int i = 0; i < edges.size(); i++) { if (edges[i].first == u && !isVisited[edges[i].second]) { dfs(edges, s, isVisited, edges[i].second); } } s.push(u); } };
這樣就得出結果了。(個人認為還是BFS易理解一點)
3.針對這道題......
So sad,以上兩種方法在這道題都TLE了,因為對於每個頂點,我們都需要遍歷一次邊的數組,要想節省時間,我們就要花費空間,使對於每個頂點,我們只需要考慮以它為起始點的邊。
真正能AC的代碼:
class Solution { public: vector<int> topologicalSort(int n, vector<pair<int, int> >& edges) { vector<int> res; vector<vector<int> > newedges(n, vector<int>()); queue<int> q; vector<int> in_degree(n, 0); for (int i = 0; i < edges.size(); i++) { in_degree[edges[i].second]++; newedges[edges[i].first].push_back(edges[i].second); } for (int i = 0; i < n; i++) { if (in_degree[i] == 0) { q.push(i); } } while (!q.empty()) { int front = q.front(); q.pop(); res.push_back(front); for (int i = 0; i < newedges[front].size(); i++) { in_degree[newedges[front][i]]--; if (in_degree[newedges[front][i]] == 0) q.push(newedges[front][i]); } } return res; } };
4.拋開這道題目——有環情況的判斷
可以利用上面的dfs方法,比如isVisited這個數組,我們可以多增一種情況,比如0為未訪問,1為已訪問,-1為正在訪問,當dfs搜索時遇到了一條邊終止頂點對應的isVisited元素為-1時,就說明圖中有環了(為-1說明我們是從這個頂點開始dfs的,現在又遇到了這個頂點...)。
另外一種判斷圖是否有環的方法,借助bfs(dfs也可,但既然用了dfs,直接用上面的方法好了),假如“生成拓撲序”后,還有頂點不在這個“拓撲序”里面,則圖就有環了(加雙引號是因為不能真正稱作“拓撲序”啊)。
LeetCode上有相應的題目,需要判斷有無環(可以試試上面兩種判斷方法):
(1)https://leetcode.com/problems/course-schedule/description/
用了上面的第二種方法:
class Solution { public: bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) { int size = 0; vector<int> degree(numCourses, 0); queue<int> q; for (int i = 0; i < prerequisites.size(); i++) { degree[prerequisites[i].second]++; } for (int i = 0; i < numCourses; i++) { if (degree[i] == 0) { q.push(i); } } while (!q.empty()) { int front = q.front(); q.pop(); size++; for (int i = 0; i < prerequisites.size(); i++) { if (prerequisites[i].first == front) { if (--degree[prerequisites[i].second] == 0) q.push(prerequisites[i].second); } } } return size == numCourses; } };
(2)https://leetcode.com/problems/course-schedule-ii/description/
用了第一種方法:
class Solution { public: vector<int> findOrder(int numCourses, vector<pair<int, int>>& prerequisites) { vector<int> res; vector<int> isVisited(numCourses, 0); vector<vector<int>> v(numCourses, vector<int>()); for (int i = 0; i < prerequisites.size(); i++) { v[prerequisites[i].second].push_back(prerequisites[i].first); } stack<int> s; bool isCircled = false; for (int i = 0; i < numCourses; i++) { if (!isVisited[i]) { dfs(i, s, v, isVisited, isCircled); } if (isCircled) { break; } } if (isCircled) return vector<int>(); while (!s.empty()) { res.push_back(s.top()); s.pop(); } return res; } void dfs(int u, stack<int> &s, vector<vector<int>>& v, vector<int> &isVisited, bool &isCircled) { if (isCircled) return; isVisited[u] = -1; for (int i = 0; i < v[u].size(); i++) { if (isVisited[v[u][i]] != 1) { if (isVisited[v[u][i]] == 0) { dfs(v[u][i], s, v, isVisited, isCircled); } else { isCircled = true; return; } } } isVisited[u] = 1; s.push(u); } };