概念上来说,它关注的是将树节点,按照高度进行合并。对一棵用edges表述的树来说,具体合并过程,相当于将每一个子节点都不断的向上合并,直到所有的节点都被压扁到他们共享一个父亲节点。这样以来如果有任何想要连接子节点,而不共享父节点的边,都是那个构成环的边,也就是冗余的边。
适用问题:
现实世界问题:
很好,很复杂。并查集本身就是一个高级的主题。在实际应用中联想,完全不如在算法问题中这么明晰,重要的是在实际中,不断的找到算法的影子。将理论和实践结合。
总的来说,并查集,和联通,冗余,分组问题相关,在遇到这些问题的时候,可以考虑运用。
该问题中,tree被定义为,无向无环图。你被给定了一个图,有 n 个节点,从 1 到 n,但是在这个图中,被加入了一条冗余的边,这条边被 1 到 n 中两个不同的端点连接。返回可以删除的边,以便生成的图是 n 节点树。如果有多个答案,则返回输入列表中最后出现的符合条件的答案。
比如,给定edges = [[1, 2],[1, 3],[2, 3]], 输出[2, 3]
其实这是一个典型的,自己组装并查集的题,通过这道题正好可以切入并查集的基础结构。代码如下:
class Solution(object):
def findRedundantConnection(self, edges):
"""
:type edges: List[List[int]]
:rtype: List[int]
"""
# 初始化父节点为该节点自己
par = [i for i in range(len(edges) + 1)]
# 将每个节点的高度设置为1
rank = [1] * (len(edges) + 1)
def find(n):
"""
helper函数,是为了在下面的步骤中,用于找到节点的父节点
while的部分表示,当自己的父节点不是自己的时候,就不断的将父节点的父节点,设置为该节点的父节点
是一种压缩树高度的行为,最终找到父节点,即为该节点自己
"""
p = par[n]
while p != par[p]:
par[p] = par[par[p]]
p = par[p]
return p
def union(n1, n2):
"""
union是用来进行合并的部分
找到这两个节点的父节点,然后比较树的高度,将低的树合并到高的树上,并重新计算高度
如果他们已经共享父节点了就不需要何必了
return的,是否进行了合并
"""
p1, p2 = find(n1), find(n2)
if p1 == p2:
return False
if rank[p1] > rank[p2]:
par[p2] = p1
rank[p1] += rank[p2]
else:
par[p1] = p2
rank[p2] += p1
return True
# 通过union返回值判断两个节点是否已经有同样的父节点
# 如果有的话说明再连接就冗余了,返回该节点即可
for n1, n2 in edges:
if not union(n1, n2):
return [n1, n2]
笔记:这是一道逻辑非常清晰的题,不再赘述。时间复杂度和空间复杂度都是O(n)。
这是因为在进行find和union的时候,它的时间复杂度都是阿克曼反函数大小。
时间复杂度O(α(n))中的α(n)是阿克曼反函数(Ackermann inverse function)的渐进记号表示。
阿克曼函数是一个数论函数,增长非常快,被用来定义计算复杂性理论中所需的差值级。α(n)是阿克曼函数的反函数,它是一种极其缓慢增长的函数。
具体来说:
因此,O(α(n))被认为是计算复杂性理论中的一个很好的渐进记号,用来表示一个非常优秀、高效的算法,它的运行时间基本上可以认为是一个很小的常数。并查集的时间复杂度就是这个量级。
总之,O(α(n))反映了一种极其高效、接近最优的算法时间复杂度,实际应用意义重大。
困难难度的力扣题。
给定一个由格子组成的二维地图,其中包含了陆地(用0表示)和水(用1表示)。在第一天都是陆地,但是每一天都有一个格子被水淹没。假设你可以在道路上自由移动,但不能斜着走。你从地图的最上方的某个起始位置出发,要前往地图的最下方。问题是在给定的淹水条件下,你哪天是最后能通过格子的。
比如cells = [[1, 1],[2, 1],[1, 2],[2, 2]],比如该cells给定的就是第index天,淹水的格子。结果应当返回2,这是因为在第三天开始无法通过格子。所以第二天是最后期限(第0天是初始天)。
同样是使用并查集思路:
以下是代码的思路步骤:
整体思路是利用并查集算法来合并连通分量,并通过维护每个连通分量的边界来判断是否满足条件。算法的核心是遍历格子并进行合并操作,以及检查合并后的连通分量的边界是否满足条件。
class Solution:
def latestDayToCross(self, row, col, cells):
n = row * col
# root是根节点,每个节点都指向自己,left和right是最左和最右边界
root, left, right = list(range(n)), [0] * n, [0] * n
# 使用格子索引更新左右边界的列表,所谓边界是指格子所在位置的col
for i in range(col):
for j in range(row):
# [0, 0, 1, 1]
left[i * row + j] = i
right[i * row + j] = i
# find找到根节点
def find(x):
if x != root[x]:
root[x] = find(root[x])
return root[x]
# 合并连通分量,返回左右边界
def union(x, y):
a, b = find(x), find(y)
if a != b:
root[a] = b
left[b] = min(left[b], left[a])
right[b] = max(right[b], right[a])
# 开始搜索
seen = set()
dirs = ((1, 0), (0, 1), (-1, 0), (0, -1),
(1, 1), (-1, 1), (1, -1), (-1, -1))
for i, cell in enumerate(cells):
cx, cy = cell[0] - 1, cell[1] - 1
for dx, dy in dirs:
x, y = cx + dx, cy + dy
#检查相邻格子的有效性和是否见过
if 0 <= x < row and 0 <= y < col and (x, y) in seen:
# union两个格子
union(cy * row + cx, y * row + x)
# 找到合并后的根节点
new = find(y * row + x)
# 检查新的组合是否可以跨越边界
if left[new] == 0 and right[new] == col - 1:
# 为真则返回天数
return i
seen.add((cx, cy))
return n
学习笔记:时间复杂度和空间复杂度O(mxn)。