有关并查集的知识及操作请参阅这篇博客。
本篇章所涉及到的题目均来源于力扣(LeetCode),著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
LeetCode中有关并查集的题目。
LeetCode第547题:朋友圈。
题目
班上有 N N N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A A A 是 B B B 的朋友, B B B 是 C C C 的朋友,那么我们可以认为 A A A 也是 C C C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N ∗ N N * N N∗N 的矩阵 M M M,表示班级中学生之间的朋友关系。如果 M [ i ] [ j ] = 1 M[i][j] = 1 M[i][j]=1,表示已知第 i i i 个和 j j j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
提示
1. 1 <= N <= 200
2. M[i][i] == 1
3. M[i][j] == M[j][i]
解题思路
如果把这个题目当做并查集来做,那么根据题目要求返回的就是最终合并成的子集合的个数,也就是数组中parent = -1的个数,即朋友圈的总数。
所以这个题的做法就是,首先初始化我们的parent数组都为 − 1 -1 −1,长度为矩阵 M M M的行数,也就是同学的个数;然后需要遍历整个矩阵,因为我们需要知道每个同学(每行
)与其他同学(该行的所有列
)是否是朋友(值为1
),如果是朋友,我们就将这位朋友(列
)划分到这个同学(行
)的朋友圈中去,即parent数组中索引为对应列的值更改为行的值;最后统计parent数组中 − 1 -1 −1的个数。
M = [ 1 1 0 1 1 1 0 1 1 ] M=\begin{bmatrix} 1 & 1 & 0 \\ 1 & 1 & 1 \\ 0 & 1 & 1 \\ \end{bmatrix} M=⎣⎡110111011⎦⎤ 以上面这个矩阵为例:
首先初始化parent数组:
同学 | 0 | 1 | 2 |
---|---|---|---|
p a r e n t parent parent | -1 | -1 | -1 |
然后开始遍历,第1行(即同学0),与同学0是朋友的是同学1,所以将parent数组中同学1的值改为0,即
同学 | 0 | 1 | 2 |
---|---|---|---|
p a r e n t parent parent | -1 | 0 | -1 |
然后遍历第2行(即同学1),与同学1是朋友的是同学0和同学2,因为我们查找可知,同学1是在同学0的朋友圈中,同学2是在同学1的朋友圈中,朋友的朋友是朋友,所以同学2也在同学0的朋友圈中,然后将parent数组中同学2的值也改为0,即
同学 | 0 | 1 | 2 |
---|---|---|---|
p a r e n t parent parent | -1 | 0 | 0 |
然后遍历第3行(即同学2),与同学2是朋友的是同学1,他们已经在同一个朋友圈里了,所以parent数组中的值不用更改,最终parent数组为
同学 | 0 | 1 | 2 |
---|---|---|---|
p a r e n t parent parent | -1 | 0 | 0 |
可以看出,parent数组中 − 1 -1 −1的个数为1,即这三位同学在同一个朋友圈中。
代码如下:
class Solution(object):
def Find(self, S, x):
while S[x] != -1:
x = S[x]
return x
def Union(self, S, root1, root2, Rank=None):
x = self.Find(S, root1)
y = self.Find(S, root2)
if x != y:
S[y] = x
def findCircleNum(self, M):
length = len(M)
S = [-1] * length
for row in range(length):
for column in range(length):
# 把自己和不是朋友的过滤掉
if row != column and M[row][column] == 1:
self.Union(S, row, column)
return S.count(-1)
运行结果如下:
LeetCode第200题:岛屿数量。
题目
给你一个由'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
解题思路
这个题我用并查集做的,算法设计的不大好,遍历了两次数组。这里的每个网格(矩阵)上的点都是一个类,所以在初始化parent数组时,需要将二维网格上点的位置与parent数组上的位置一一对应,就是矩阵按行优先的方式转换成一维数组,具体可以参考这篇博客;然后遍历矩阵,将相邻的'1'
归并到一起;最后统计每个集合的个数,就是找每个集合的 B O S S BOSS BOSS,如果网格点对应parent数组的 i n d e x index index与它所属的集合号码一样,那该点就是集合的 B O S S BOSS BOSS。
代码如下:
class Solution(object):
def Find(self, S, x):
while S[x] != -1:
x = S[x]
return x
def Union(self, S, root1, root2, Rank=None):
x = self.Find(S, root1)
y = self.Find(S, root2)
if x != y:
# S[y] = x
# 按秩合并
if Rank[x] > Rank[y]:
S[y] = x
elif Rank[x] < Rank[y]:
S[x] = y
else:
# 谁并入谁都行, 并入到哪里, 哪里的秩就要加1
S[y] = x
Rank[x] += 1
def numIslands(self, grid):
if not grid:
# LeetCode的这个操作太那啥了
# 竟然放一个空数组进来
return 0
row = len(grid)
column = len(grid[0])
S = [-1] * (row * column)
Rank = [1] * (row * column)
for i in range(row):
for j in range(column):
if grid[i][j] == '1':
# [i-1, j]
# [i, j-1] [i, j] [i, j+1]
# [i+1, j]
for direction in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
if 0 <= i+direction[0] < row and 0 <= j + direction[1] < column \
and grid[i + direction[0]][j + direction[1]] == '1':
self.Union(S, i * column + j, (i+direction[0]) * column + (j+direction[1]), Rank)
count = 0
for i in range(row):
for j in range(column):
if grid[i][j] == '1':
index = i * column + j
# 寻找集合的boss
if self.Find(S, index) == index:
count += 1
return count
运行结果如下:
后来想想又对第二次遍历做了一个切片,然而效果并没有大的提升:
count = 0
for index in range(row * column):
i = index // column
j = index % column
if grid[i][j] == '1':
# 寻找集合的boss
if self.Find(S, index) == index:
count += 1
LeetCode第990题:等式方程的可满足性。
题目
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程equations[i]
的长度为4
,并采用两种不同的形式之一:"a==b"
或 "a!=b"
。在这里, a a a 和 b b b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true
,否则返回 false
。
解题思路
这个题目是通过"=="
号建立的连接关系,如果两者相等,说明这两个字符属于同一个集合;如果"!="
号两边的字符在一个集合里面,那就说明这两个字符中间应该是"=="
号,这时候就要返回false
,否则返回true
。
代码如下:
class Solution(object):
def Find(self, S, x):
while S[x] != -1:
x = S[x]
return x
def Union(self, S, root1, root2, Rank=None):
x = self.Find(S, root1)
y = self.Find(S, root2)
if x != y:
# S[y] = x
# 按秩合并
if Rank[x] > Rank[y]:
S[y] = x
elif Rank[x] < Rank[y]:
S[x] = y
else:
# 谁并入谁都行, 并入到哪里, 哪里的秩就要加1
# 这里是root2并入root1中
S[y] = x
Rank[x] += 1
def equationsPossible(self, equations):
# 将每个字母映射到数字
S = [-1] * 26
Rank = [1] * 26
for equation in equations:
if equation[1] == '=':
self.Union(S, ord(equation[0]) - ord('a'), ord(equation[3]) - ord('a'), Rank)
for equation in equations:
if equation[1] == '!' and self.Find(S, ord(equation[0]) - ord('a')) == self.Find(S, ord(equation[3]) - ord('a')):
return False
return True
运行结果如下:
这个题感觉不用将字母映射也可以,只不过需要将parent数组换成一个字典,应该是可行的,有时间再试试。
通过这几个题可以发现,并查集类的题目大致思路都一样,就是根据题目建立集合,然后将有连接的集合进行归并。我觉得难点就是集合归并后,如何选择合适的方法去处理它来得到正确的结果,这个方法的选择貌似就决定了算法的复杂性,继续加油(ง •̀_•́)ง