题目
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 most 10000.
- 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].
思路与解法
如果从一个点S
出发的所有路径Paths
最终都到达某个终点Ts
(可以不为同一个终点),则这个点S
即为最终安全的点(Eventually Safe)。这道题目的要求是让我们找出所求满足这样条件的点。
此题目与310. Minimum Height Trees方法二解法类似,同样可以采用剥洋葱的思想:
- 最外围的点(出度为0)势必满足条件,然后将所有与最外围节点相连的边删除,此时次外围的点可能就会变为最外围的点(之所以是“可能”,是因为出度减1后仍然不为0或者次外围的点会和其他点形成环而造成出度永远不为0);
- 然后利用递归的思想不断删减掉最外围的点及与其相连的边,暴露出次外围的点,直到剩下的点中没有出度为0的点;
- 在计算过程中,不断删减掉的点即为满足条件的点,将其存放到数组中最终进行排序即可。
代码实现
此算法我使用go语言实现
// nodes 切片存放最终满足条件的节点
// outDegree 统计每个节点的出度
// prevNodes 存放每个节点的前向边的节点
// queue 利用队列来实现“剥洋葱”
func eventualSafeNodes(graph [][]int) []int {
nodes := make([]int, 0)
outDegree := make(map[int]int)
prevNodes := make(map[int][]int)
queue := make([]int, 0)
for u, vs := range graph {
outDegree[u] = len(vs) // 节点u的出度即为len(vs)
if outDegree[u] == 0 { // 当outDegree[u]的初度为零时,将u加入到nodes、queue中
queue = append(queue, u)
nodes = append(nodes, u)
}
for _, v := range vs { // 统计前向边
prevNodes[v] = append(prevNodes[v], u)
}
}
// 利用队列的入队出队来实现“递归的思想”
for len(queue) != 0 {
node := queue[0] // 获取队列头节点
queue = queue[1:] // 出队
for _, anotherNode := range prevNodes[node] { // 遍历与node相连的前向边
outDegree[anotherNode]--
if outDegree[anotherNode] == 0 { // 当anotherNode的初度为零时,将anotherNode加入到nodes、queue中
nodes = append(nodes, anotherNode)
queue = append(queue, anotherNode)
}
}
}
sort.Ints(nodes) // 将nodes进行从大到小排序
return nodes
}
总结与思考
在算法分析与设计的过程中,我们首先提取题目的主要信息,并对题目进行一定的抽象;通常上题目并不会直观的考察某个知识点。所以,这就需要我们进行一定的类比,将某种算法应用到实际的题目中。以这道题目为例,“剥洋葱”是生活中的一个例子,其思想经过一定的抽象即可应用的该题目中;另外,这也是一种递归的想法,每个子问题都和父问题除了规模外都一致;最后,实现递归的思想不一定非要用递归函数,队列的入队出队也可以实现,写法同样很简便。