在有向图中, 我们从某个节点和每个转向处开始, 沿着图的有向边走。 如果我们到达的节点是终点 (即它没有连出的有向边), 我们停止。
现在, 如果我们最后能走到终点,那么我们的起始节点是最终安全的。 更具体地说, 存在一个自然数 K
, 无论选择从哪里开始行走, 我们走了不到 K
步后必能停止在一个终点。
哪些节点最终是安全的? 结果返回一个有序的数组。
该有向图有 N
个节点,标签为 0, 1, ..., N-1
, 其中 N
是 graph
的节点数. 图以以下的形式给出: graph[i]
是节点 j
的一个列表,满足 (i, j)
是图的一条有向边。
示例: 输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]] 输出:[2,4,5,6] 这里是上图的示意图。
提示:
graph
节点数不超过 10000
.32000
.graph[i]
被排序为不同的整数列表, 在区间 [0, graph.length - 1]
中选取。记得有拓扑排序,可以在有向无环图中找到一条线性序列,主要的idea是利用了节点的入度(indegree),那么这道题如果稍加分析,就可以发现其实可以用出度(outdegree)来解题。对于出度为0的节点,肯定是安全节点,我们尝试把他们从图中删去,同时也删去和他们链接的边,这时再寻找出度为0的节点,这些节点的下一个节点只能跳到安全节点,所以他们肯定也是安全节点,所以我们再把他们从图中删去,同样的把链接他们的边从图中删去。。。我们一直这样操作知道再也找不到这样的节点,这时图中只会剩下一个个的环(首尾相连,不存在出度为0的节点),那么我们删去的点就是安全节点。
思路一:思路有了,那么怎么实现是个问题,根据上面的说明,我们需要逆向构建出图的每个节点的入边邻接表,因为每次删除节点需要把对应的入边删除(我们可以直接删除入边节点对应的出度),这样构建入边邻接表需要O(V+E),V是节点数,E是边数。每次寻找出度为0的节点需要O(V),我们最多需要寻找V次,所以总体时间复杂度为O(V^2+V+E)。
参考代码:
class Solution {
public:
pair has_out_degree_zero(vector &out_degree) {
for (int i = 0; i < out_degree.size(); i++) {
if (out_degree[i] == 0) return { true,i };
}
return { false,-1 };
}
vector eventualSafeNodes(vector>& graph) {
vector res;
if (graph.empty()) return res;
vector out_degree(graph.size(), 0);
vector> in_edges(graph.size());
vector> out_edges=graph;
for (int i = 0; i < graph.size(); i++) {
if (!graph[i].empty()) {
out_degree[i] = graph[i].size();
for (int j = 0; j < graph[i].size(); j++) {
in_edges[graph[i][j]].push_back(i);
}
}
}
while (has_out_degree_zero(out_degree).first) {
int outDegree_zero_index = has_out_degree_zero(out_degree).second;
out_degree[outDegree_zero_index] = -1;
res.push_back(outDegree_zero_index);
for (auto in_edge : in_edges[outDegree_zero_index]) {
out_degree[in_edge]--;
}
}
sort(res.begin(), res.end());
return res;
}
};
思路二:我们不需要构建节点的入边邻接表,我们只需要构建有向图的逆有向图,即把原来从节点i指向节点j的边转变为:节点j指向节点i。由此可以构造出正向图right_graph和逆向图reverse_graph。同时需要一个辅助队列q,初始化先把正向图中出度为0的节点放入q中,然后当队列不为空时做如下循环(取出对头元素top,通过逆向图找到top的邻边节点集合set_edges(相当于top节点的入边节点),然后在正向图中把邻边点集合和top相邻的边删除,这样也是一种变相的删除,和上面的思路一样),这样我们把邻接表设计成hash_set,那么查找和删除操作都是O(1),总体时间可以做到O(V+E),主要是在需要构建正向图和逆向图的时间。
参考代码:
class Solution {
public:
vector eventualSafeNodes(vector>& graph) {
if (graph.empty()) return {};
vector res;
vector visited(graph.size(),false);
vector> right_graph(graph.size());
vector> reverse_graph(graph.size());
queue q;
for (int i = 0; i < graph.size(); i++) {
if (graph[i].empty()) q.push(i);
for (int j : graph[i]) {
right_graph[i].insert(j);
reverse_graph[j].insert(i);
}
}
while (!q.empty()) {
int node = q.front(); q.pop();
visited[node] = true;
for (auto neibor_node : reverse_graph[node]) {
right_graph[neibor_node].erase(node);
if (right_graph[neibor_node].empty()) q.push(neibor_node);
}
}
for (int i = 0; i < graph.size(); i++) {
if (visited[i]) res.push_back(i);
}
return res;
}
};
思路三:用DFS做,传统的“白-灰-黑”(白代表未访问,灰代表在一个dfs中已经访问过,黑代表之前已经访问过的肯定不会成环的节点)三色法可以用来寻找一个图中是否存在环,主要思路是:对每一个节点做dfs操作,在进入是染色为灰,如果dfs过程中遇到灰色的节点,证明在这个dfs中访问的节点中一定存在环,如果没有环,在dfs退出时把dfs的入节点染色黑。因为入节点不一定在环中,所以我们必须对每一个节点都做dfs操作,这样复杂度很高。我们想办法来降低复杂度。
我们把dfs返回值设计成bool类型的,如果其邻边的任意一个路径存在环,我们就返回false,但是颜色保留不变不清零。这样做可以实现如果这个节点的dfs存在环,那么他就会被灰色标示,这样下次再遍历到这个节点时就已经知道这个节点存在环,不在遍历,可以大幅降低复杂度。
参考代码:
bool dfs(vector>& graph, vector &colors, int i) {
//0 white 1 grey 2 black
if (colors[i] != 0) return colors[i] == 2;
colors[i] = 1;
for (auto neibor : graph[i]) {
if (colors[neibor] == 2) continue;
if (colors[neibor] == 1 || !dfs(graph, colors, neibor)) return false;
}
colors[i] = 2;
return true;
}
vector eventualSafeNodes(vector>& graph) {
vector res;
vector colors(graph.size(), 0);
for (int i = 0; i < graph.size(); i++) {
if (dfs(graph, colors, i)) res.push_back(i);
}
return res;
}