leetcode刷题笔记——并查集
目前完成的并查集相关的leetcode算法题序号:
中等:721,684,1585,1631
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reconstruct-itinerary
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
文章目录
算法理解
并查集(union-find, 或disjoint set)可以动态地连通两个点,并且可以非常快速地判断两个点是否连通。假设存在n 个节点,我们先将所有节点的父亲标为自己;每次要连接节点i 和j 时,我们可以将i 的父亲标为j;每次要查询两个节点是否相连时,我们可以查找i 和j 的祖先是否最终为同一个人。
提示:以下是并查集相关的刷题笔记
一、721题:账户合并
并查集构建的三个核心部分:
1)构建对象的父节点索引列表;
2)将同一个集合中的对象,通过构建父子节点关系的方式进行关联;
2)递归查找各个对象的根节点对象;
1.题干
给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。
现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。
合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是按顺序排列的邮箱地址。账户本身可以以任意顺序返回。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/accounts-merge
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
2.思路
本次的解题思路包括五个步骤:
1)对目标进行编码,确定有几个不同的对象;
2)根据各个对象所在的集合,生成各个对象之间的父子节点关系;
3)将各个对象的编码结果作为索引值,生成父节点的索引列表;
4)利用递归的方式查找,通过各个对象的父节点的索引,查找各个对象的根对象节点;
5)通过根对象节点,对对象集合进行划分;
3.代码
class UnionFind:#并查集数据结构定义
def __init__(self, n):
#需要构建的父节点索引列表初始化,需要长度参数,此处长度参数即为不同邮件的数目
#初始化各个对象的索引都是自身,即都是根节点
self.parent = list(range(n))
def find(self, index:int) -> int:#查找对象节点的根节点对象
if self.parent[index] != index:#如果某个对象的父节点索引不是自身,即不是根节点
self.parent[index] = self.find(self.parent[index])#则递归的去查找它的父节点的父节点,直到找到根节点对象
return self.parent[index]
def union(self, index1:int, index2:int):
#将两个对象通过将其各自的根节点构建父子节点的方式,进行关联
self.parent[self.find(index2)] = self.find(index1)#将第一个对象的根节点作为第二个对象的根节点的父节点
class Solution:
def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
emailToIndex = dict()
emailToName = dict()
for account in accounts:
#初始化邮件对象到数值索引值的映射
#初始化邮件对象到用户名称的映射
name = account[0]
for email in account[1:]:
if email not in emailToIndex:
emailToIndex[email] = len(emailToIndex)
emailToName[email] = name
uf = UnionFind(len(emailToIndex))
#根据各个账户数据(对象的集合),将各个对象关联起来,绘制图
for account in accounts:
firstIndex = emailToIndex[account[1]]
for email in account[2:]:
uf.union(firstIndex, emailToIndex[email])
#生成同一个用户的邮件对象集合,即为各个同一个根节点对象的所有邮件对象集合
indexToEmail = collections.defaultdict(list)
for email, index in emailToIndex.items():
index = uf.find(index)
indexToEmail[index].append(email)
res = []
#根据各个账户的邮件集合,选择集合中的一个邮件对账户名称进行索引,合并排序后的邮件对象,即得到结果
for emails in indexToEmail.values():
res.append([emailToName[emails[0]]] + sorted(emails))
return res
二、684题:冗余连接
并查集构建的三个核心部分:
1)构建对象的父节点索引列表;
2)将同一个集合中的对象,通过构建父子节点关系的方式进行关联;
2)递归查找各个对象的根节点对象;
1.题干
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, …, N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/redundant-connection
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
2.思路
核心还是采用并查集的方法。
本次的解题思路包括五个步骤:
1)确定整个图中的节点数量,创建各个点的父节点的索引列表;
2)由于各个节点的取值为1~N,可以直接作为父节点的索引列表的索引值,根据各个边,构建各个节点之间的关联关系;
3)寻找各个边的两个节点的根节点,如果根节点不一致,则关联两个根节点,如果根节点一致,则返回这条边,得到结果;
3.代码
class UnionFind:#并查集数据结构定义
def __init__(self, n):
#需要构建的父节点索引列表初始化,需要长度参数,此处长度参数即为不同邮件的数目
#初始化各个对象的索引都是自身,即都是根节点
self.parent = list(range(n+1))#注意这里索引列表的范围为0~N,因为节点是从1开始的
def find(self, index:int) -> int:#查找对象节点的根节点对象
if self.parent[index] != index:#如果某个对象的父节点索引不是自身,即不是根节点
self.parent[index] = self.find(self.parent[index])#则递归的去查找它的父节点的父节点,直到找到根节点对象
return self.parent[index]
def union(self, index1:int, index2:int):
#如果这条边的两个几点的根节点不一致,将两个对象通过将其各自的根节点构建父子节点的方式,进行关联
#将第一个对象的根节点作为第二个对象的根节点的父节点
if self.find(index1) != self.find(index2):
self.parent[self.find(index2)] = self.find(index1)
else:
#如果这条边对应的两个节点的根节点一致,则返回这条边作为结果
return [index1, index2]
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
li = [n for a in edges for n in a]#将二维列表展开
lens = len(set(li))#去除重复值,得到节点数量
uf = UnionFind(lens)
for edge in edges:#对每条边进行查看,如果边的两个节点的根节点一样,则返回结果,不一样则关联这两个根节点
detect_res = uf.union_detect(edge[0],edge[1])
if detect_res:
return detect_res
4.路径压缩的并查集
为了避免生成一条很长的路径,从而导致寻找某个节点的根节点的时间复杂度大大增加的情况,需要对并查集的路径进行压缩,每次合并两个节点的根节点的时候,根据两个根节点的深度来确定将哪一个根节点作为另一个根节点的父节点
class UnionFind:
def __init__(self, n):
self.parent = list(range(n+1))
self.rank = [0] * (n+1)#建立各个节点作为根节点的树的高度索引列表
def find(self, index):
if self.parent[index] != index:
self.parent[index] = self.find(self.parent[index])
return self.parent[index]
def union_detect(self, index1, index2):
index1_root = self.find(index1)
index2_root = self.find(index2)
if index1_root != index2_root:#每次关联两个根节点的时候,首先判断两个根节点的rank值
#将rank值大的根节点作为另一个根节点的父节点
if self.rank[index1_root] > self.rank[index2_root]:
self.parent[index2_root] = index1_root
self.rank[index1_root] += 1
else:
self.parent[index1_root] = index2_root
self.rank[index2_root] += 1
else:
return [index1, index2]
三、1585题:连接所有点的最小费用
1.题干
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
2.思路
抽丝剥茧之后,本质就是图网络中的最小生成树问题。
思路要点:
1)求解出所有点两两之间的距离,得到任意两点之间的距离和对应两个点的编号元组构成的列表,按照距离排序;
2)利用贪心的思想,从最小的边开始看,如果这条边加入到树结构中,不会引入环,则将这条边加入树中,直到引入n-1条边(n为点的数量),即可得到最小生成树
3.代码
##并查集数据结构和相关函数的定义(带压缩路径部分)
class UnionFind(object):
def __init__(self, n):
self.parent = list(range(n))
self.rank = [1] * n
def find(self, point):
if self.parent[point] != point:
self.parent[point] = self.find(self.parent[point])
return self.parent[point]
def union(self, point1, point2):
point1_root = self.find(point1)
point2_root = self.find(point2)
if point1_root == point2_root:
return False
else:
if self.rank[point1_root] > self.rank[point2_root]:
self.parent[point2_root] = point1_root
elif self.rank[point1_root] < self.rank[point2_root]:
self.parent[point1_root] = point2_root
else:
self.parent[point2_root] = point1_root
self.rank[point1_root] += 1
return True
##题解部分
class Solution:
def minCostConnectPoints(self, points: List[List[int]]) -> int:
#定义求解两点之间曼哈顿距离的函数
dist = lambda x,y : abs(x[0]-y[0]) + abs(x[1]-y[1])
points_dis = []
counts = len(points)
res = 0
#求解两两之间的曼哈顿距离,并与点的编号构成元组加入到列表中
for i in range(len(points)-1):
for j in range(i+1, len(points)):
points_dis.append([dist(points[i],points[j]), i, j])
#按照两点之间的距离升序排列,从最短的边开始查找
points_dis.sort()
uf = UnionFind(len(points))
#如果某条边引入树结构中,不会产生环路,则将其加入到树结构中,通过counts计数以及找到的边,n个点的最小生成树包括n-1条边
for point in points_dis:
if uf.union(point[1], point[2]):
res += point[0]
counts -= 1
if counts == 1:
return res
return res
四、1631题:最小体力消耗路径
1. 题干
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
2. 解题思路
本题除了深度优先/广度优先搜索(结合二分法或者最优路径思想)求解之外,还可以使用并查集。
1)将整个地形分布看作一张图,各个位置点作为图中的节点,将两点的高度之差的绝对值作为两个点之间连线的边的权重,首先遍历地形数据,得到各个点与上下左右相邻的四个点之间的权重;
2)将边的集合,按照权重升序排序;
3)遍历排序后的边的集合,从权重最小的边开始,连接边对应的两个点,每次连接后判断左上角和右下角是否连接,连接了则直接返回最后连接的边的权重;
3. 代码
#并查集定义模板
class UnionFind(object):
def __init__(self, n):
self.parent = list(range(n))
self.rank = [1] * n
def find(self, index):
if self.parent[index] != index:
self.parent[index] = self.find(self.parent[index])
return self.parent[index]
def union(self, index1, index2):
index1_root = self.find(index1)
index2_root = self.find(index2)
if self.rank[index1_root] > self.rank[index2_root]:
self.parent[index2_root] = index1_root
elif self.rank[index1_root] < self.rank[index2_root]:
self.parent[index1_root] = index2_root
else:
self.parent[index2_root] = index1_root
self.rank[index1_root] += 1
def collection(self, index1, index2):
return self.find(index1) == self.find(index2)
class Solution:
def minimumEffortPath(self, heights: List[List[int]]) -> int:
m = len(heights)
n = len(heights[0])
uf = UnionFind(m*n)
coll_dic = []
#遍历地形矩阵,生成各个权重边
for i in range(m):
for j in range(n):
pos = i * n + j
if i < m - 1:
coll_dic.append([abs(heights[i+1][j] - heights[i][j]), pos, pos + n])
if j < n - 1:
coll_dic.append([abs(heights[i][j+1] - heights[i][j]), pos, pos + 1])
#对权重边集合按照权重值升序排序
coll_dic.sort()
#遍历排序后的权重边集合,依次连接,连接后判断左上角和右下角是否连接,连接了直接返回最后连接的边的权重
for li in coll_dic:
uf.union(li[1], li[2])
if uf.collection(0, m*n-1):
return li[0]
#补充地形数据为1*1的情况
return 0