Find Eventual Safe States 找到最终的安全状态

在有向图中, 我们从某个节点和每个转向处开始, 沿着图的有向边走。 如果我们到达的节点是终点 (即它没有连出的有向边), 我们停止。

现在, 如果我们最后能走到终点,那么我们的起始节点是最终安全的。 更具体地说, 存在一个自然数 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]
这里是上图的示意图。

Illustration of graph

提示:

  • 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<bool,int> has_out_degree_zero(vector<int> &out_degree) {
	for (int i = 0; i < out_degree.size(); i++) {
		if (out_degree[i] == 0) return { true,i };
	}
	return { false,-1 };
}
vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
	vector<int> res;
	if (graph.empty()) return res;
	vector<int> out_degree(graph.size(), 0);
	vector<vector<int>> in_edges(graph.size());
	vector<vector<int>> 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<int> eventualSafeNodes(vector<vector<int>>& graph) {
	if (graph.empty()) return {};
	vector<int> res;
	vector<bool> visited(graph.size(),false);
	vector<unordered_set<int>> right_graph(graph.size());
	vector<unordered_set<int>> reverse_graph(graph.size());
	queue<int> 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<vector<int>>& graph, vector<int> &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<int> eventualSafeNodes(vector<vector<int>>& graph) {
	vector<int> res;
	vector<int> colors(graph.size(), 0);
	for (int i = 0; i < graph.size(); i++) {
		if (dfs(graph, colors, i)) res.push_back(i);
	}
	return res;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值