In a directed graph, we start at some node and every turn, walk along a directed edge of the graph. If we reach a node that is terminal (that is, it has no outgoing directed edges), we stop.
Now, say our starting node is eventually safe if and only if we must eventually walk to a terminal node. More specifically, there exists a natural number K
so that for any choice of where to walk, we must have stopped at a terminal node in less than K
steps.
Which nodes are eventually safe? Return them as an array in sorted order.
The directed graph has N
nodes with labels 0, 1, ..., N-1
, where N
is the length of graph
. The graph is given in the following form: graph[i]
is a list of labels j
such that (i, j)
is a directed edge of the graph.
Example: Input: graph = [[1,2],[2,3],[5],[0],[5],[],[]] Output: [2,4,5,6] Here is a diagram of the above graph.
Note:
graph
will have length at most10000
.- The number of edges in the graph will not exceed
32000
. - Each
graph[i]
will be a sorted list of different integers, chosen within the range[0, graph.length - 1]
.
這道題給了我們一個有向圖,然后定義了一種最終安全狀態的結點,就是說該結點要在自然數K步內停止,所謂停止的意思,就是再沒有向外的邊,即沒有出度,像上面例子中的結點5和6就是出度為0,因為graph[5]和graph[6]均為空。那么我們分析題目中的例子,除了沒有出度的結點5和6之外,結點2和4也是安全狀態結點,為啥呢,我們發現結點2和4都只能到達結點5,而結點5本身就是安全狀態點,所以2和4也就是安全狀態點了,所以我們可以得出的結論是,若某結點唯一能到達的結點是安全狀態結點的話,那么該結點也同樣是安全狀態結點。那么我們就可以從沒有出度的安全狀態往回推,比如結點5,往回推可以到達結點4和2,先看結點4,此時我們先回推到結點4,然后將這條邊斷開,那么此時結點4出度為0,則標記結點4也為安全狀態結點,同理,回推到結點2,斷開邊,此時結點2雖然入度仍為2,但是出度為0了,標記結點2也為安全狀態結點。
分析到這里,思路應該比較明朗了,由於我們需要回推邊,所以需要建立逆向邊,用一個集合數組來存,由於題目要求返回的結點有序,我們可以利用集合TreeSet的自動排序的特性,由於需要斷開邊,為了不修改輸入數據,所以我們干脆再建一個順向邊得了,即跟輸入數據相同。還需要一個safe數組,布爾型的,來標記哪些結點是安全狀態結點。在遍歷結點的時候,直接先將出度為0的安全狀態結點找出來,排入一個隊列queue中,方便后續的處理。后續的處理就有些類似BFS的操作了,我們循環非空queue,取出隊首元素,標記safe中該結點為安全狀態結點,然后遍歷其逆向邊的結點,即可以到達當前隊首結點的所有結點,我們在正向邊集合中刪除對應的邊,如果此時結點出度為0了,將其加入隊列queue中等待下一步處理,這樣while循環退出后,所有的安全狀態結點都已經標記好了,我們直接遍歷safe數組,將其存入結果res中即可,參見代碼如下:
解法一:
class Solution { public: vector<int> eventualSafeNodes(vector<vector<int>>& graph) { vector<int> res; int n = graph.size(); vector<bool> safe(n, false); vector<set<int>> g(n, set<int>()), revg = g; queue<int> q; for (int i = 0; i < n; ++i) { if (graph[i].empty()) q.push(i); for (int j : graph[i]) { g[i].insert(j); revg[j].insert(i); } } while (!q.empty()) { auto t = q.front(); q.pop(); safe[t] = true; for (int i : revg[t]) { g[i].erase(t); if (g[i].empty()) q.push(i); } } for (int i = 0; i < n; ++i) { if (safe[i]) res.push_back(i); } return res; } };
我們再來看一種DFS遍歷有向圖的解法。仔細分析題目中的例子,不難發現,之所以某些結點不是安全狀態,因為有環的存在,而環經過的所有結點,一定不是安全狀態結點,所以我們可以通過DFS遍歷有向圖來找出環即可。在大多數的算法中,經典的DFS遍歷法對於結點都有三種狀態標記,white,gray,和black,其中white表示結點還未遍歷,gray表示正在遍歷鄰結點,black表示已經結束該結點的遍歷。那么我們可以對每個結點都調用遞歸函數,在遞歸函數中,如果當前結點不是white,表示該結點已經訪問過了,那么如果當前結點是black,直接返回true,如果是gray,直接返回false,因為遇到gray的結點,表示一定有環存在。否則我們給結點標記gray,然后開始遍歷所有鄰接結點,如果某個鄰結點是black,直接跳過該結點。如果某個鄰結點是gray,或者對該鄰結點調用遞歸返回false了,說明當前結點是環結點,返回false。如果循環結束了,當前結點標記為black,並且返回true,參見代碼如下:
解法二:
class Solution { public: vector<int> eventualSafeNodes(vector<vector<int>>& graph) { int n = graph.size(); vector<int> res, color(n); // 0 white, 1 gray, 2 black for (int i = 0; i < n; ++i) { if (helper(graph, i, color)) res.push_back(i); } return res; } bool helper(vector<vector<int>>& graph, int cur, vector<int>& color) { if (color[cur] > 0) return color[cur] == 2; color[cur] = 1; for (int i : graph[cur]) { if (color[i] == 2) continue; if (color[i] == 1 || !helper(graph, i, color)) { return false; } } color[cur] = 2; return true; } };
參考資料:
https://leetcode.com/problems/find-eventual-safe-states/solution/