1. 问题描述:
在本问题中,有根树指满足以下条件的有向图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示有向图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
示例 1:
输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]
示例 2:
输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]
提示:
n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ui, vi <= n
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/redundant-connection-ii
2. 思路分析:
这道题目与力扣684题是类似的,这里是有向图,其实多了一个限制条件之后就会变得比较复杂了,需要考虑的情况也比较多,主要分为以下三种情况进行讨论:
- 在树中加入了某条边之后形成了环,也即某个节点的边连上了根节点,这个时候去掉环中的任意一个点即可
- 在树中加入了某条边之后没有形成环但是存在了入度为2的边,这个时候我们需要去掉入度为2的其中一条边即可,去掉其中一条边使得某个节点的父节点只有一个(有两个父节点的图不是树)
- 存在上面两种情况,某个节点连上了它的祖先节点,这个时候只能够去掉连向祖先节点的边
我们的任务是需要找出图加入的这条边之后的图对应的是哪一种情况,根据属于哪一种情况返回需要去除哪一条边即可。具体来说我们需要解决两个问题:
- 判断某条边是否是在环中
- 判断某条边是否是入度为2的边之一
如果上面两种情况都存在那么属于第三种情况,否则属于第一种或者是第二种情况。首先我们可以比较简单的情况,判断某条边是否是入度为2的边之一,这个其实比较好解决,我们只需要枚举整个图,判断当前a==>b的中的点b之前是否被访问过了(使用一个辅助数组p来进行标记),访问过了说明当前点的入度为2,所以标记这两条边是度数为2的入边即可(使用st2数组或者列表来记录,st[i]表示第i条边是否是度数为2的入边之一)。
接下来是一个比较麻烦的事情,也就是在有向图中找环,并且找到环之后在数组或者是列表中标记哪一条边是是环中的边,有向图中找环可以使用深搜,在深搜的过程中需要声明比较多的辅助数组来帮助我们找环,我们其实是可以通过一维数组中标记某个点是否在环中然后枚举图中的边,当当前的边对应的两个点都是环中的点表示当前的边就是环中的边,那么标记当前的边是环中的边即可。在有向图中找环是一个经典的算法,我们其实理解其中的原理,记住其中的模板在需要的时候默写出来即可。其中需要声明一个栈stk(python是列表)来存储环中点,列表st标记dfs搜索过程中访问过的点,in_c用来记录某个点是否在环中,在dfs的过程中找环思想其实很简单,就是在搜索的过程中发现当前要搜索的节点之间已经搜索过了,那么说明出现了环,而在搜索的时候使用stk来记录节点,所以这个时候stk中不等于当前要搜索的点的就是环中的点,循环枚举即可,我们找到了环之后需要将在环中的所有点标记到in_c数组或者是列表中,后面枚举图中每一条边的时候判断是否当前的两个点是否同时标记在in_c数组即可。
3. 代码如下:
from typing import List
import collections
class Solution:
# 使用深搜来找环, 当某次搜索的过程中出现了之前已经标记过的点说明了存在出现了环
def dfs(self, g: collections.defaultdict, u: int):
self.st[u] = 1
self.stk.append(u)
for t in g[u]:
# 当前点没有搜过
if self.st[t] == 0:
if self.dfs(g, t): return True
# 当前点搜过了说明说明出现了环, 我们之前在搜索的时候使用一个栈stk来存储搜索过的元素, 所以这个时候栈中不等于t的元素就是环中的元素, 标记一下
elif self.st[t] == 1:
while self.stk[-1] != t:
# 标记环中的所有点, 这样后面可以根据这个数组找出环中的所有边
self.in_c[self.stk.pop()] = 1
# 加入环中的最后一个节点
self.in_c[t] = 1
# 找到环返回True
return True
# 回溯, 说明没有找到环
self.st[u] = 0
self.stk.pop()
return False
def work1(self, edges: List[List[int]], n: int):
# 深搜需要根据节点的编号来搜索, 而且因为使用的是python语言, 所以需要将点存储到字典中, 键表示节点编号, 值表示节点编号的出边编号
_edges = collections.defaultdict(list)
for e in edges:
a, b = e[0], e[1]
_edges[a].append(b)
for i in range(1, n + 1):
# 判断当前起点出发是否存在环, 找到了就break了
if self.dfs(_edges, i):
break
for i in range(n):
a, b = edges[i][0], edges[i][1]
# 根据深搜的结果找出环中的所有边, 之前dfs搜索的时候标记了环的点这里根据环中的点找到对应的边, 只有当前的边的两个点都在环中说明当前的边才是环中的边
if self.in_c[a] and self.in_c[b]:
self.st1[i] = 1
# 求解边是否是入度为2的边之一
def work2(self, edges: List[List[int]], n: int):
p = [-1] * (n + 1)
for i in range(n):
a, b = edges[i][0], edges[i][1]
if p[b] != -1:
self.st2[p[b]] = self.st2[i] = 1
break
p[b] = i
# st1[i]表示当前第i条边是否在环中, st2[i]表示当前的第i条边是否是度数为2的入边之一, st表示标记数组标记在搜索过程中已经访问过的点
# stk用来存储环中元素
st1, st2, stk, in_k, st, in_c = None, None, None, None, None, None
def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
self.st1, self.st2 = [0] * (n + 1), [0] * (n + 1)
self.st, self.in_c = [0] * (n + 1), [0] * (n + 1)
self.stk = list()
self.work1(edges, n)
self.work2(edges, n)
# 先判断是否是第三种情况, 也即属于存在环并且环中某个点的入度为2, 因为求解的是最优一条边所以在判断属于哪一种情况都需要逆序遍历
for i in range(n - 1, -1, -1):
if self.st1[i] and self.st2[i]:
return edges[i]
# 不是第三种情况说明属于第一种情况或者是第二种情况, 只要找到满足属于其中一种情况那么直接返回即可
for i in range(n - 1, -1, -1):
if self.st1[i] or self.st2[i]: return edges[i]
return []