Python描述数据结构之并查集实战篇

前言

  有关并查集的知识及操作请参阅这篇博客

  本篇章所涉及到的题目均来源于力扣(LeetCode),著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
  LeetCode中有关并查集的题目。

1. LeetCode547:朋友圈

  LeetCode第547题:朋友圈

  题目 班上有 N N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A A B B 的朋友, B B C C 的朋友,那么我们可以认为 A A 也是 C C 的朋友。所谓的朋友圈,是指所有朋友的集合。
  给定一个 N N N * N 的矩阵 M M ,表示班级中学生之间的朋友关系。如果 M [ i ] [ j ] = 1 M[i][j] = 1 ,表示已知第 i i 个和 j j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
  提示

1. 1 <= N <= 200
2. M[i][i] == 1
3. M[i][j] == M[j][i]

  解题思路 如果把这个题目当做并查集来做,那么根据题目要求返回的就是最终合并成的子集合的个数,也就是数组中parent = -1的个数,即朋友圈的总数。
  所以这个题的做法就是,首先初始化我们的parent数组都为 1 -1 ,长度为矩阵 M M 的行数,也就是同学的个数;然后需要遍历整个矩阵,因为我们需要知道每个同学(每行)与其他同学(该行的所有列)是否是朋友(值为1),如果是朋友,我们就将这位朋友()划分到这个同学()的朋友圈中去,即parent数组中索引为对应列的值更改为行的值;最后统计parent数组中 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}   以上面这个矩阵为例:

  首先初始化parent数组:

同学 0 1 2
p a r e n t parent -1 -1 -1

  然后开始遍历,第1行(即同学0),与同学0是朋友的是同学1,所以将parent数组中同学1的值改为0,即

同学 0 1 2
p a r e n t 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 -1 0 0

  然后遍历第3行(即同学2),与同学2是朋友的是同学1,他们已经在同一个朋友圈里了,所以parent数组中的值不用更改,最终parent数组为

同学 0 1 2
p a r e n t parent -1 0 0

  可以看出,parent数组中 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)

  运行结果如下:

在这里插入图片描述

2. LeetCode200:岛屿数量

  LeetCode第200题:岛屿数量

  题目 给你一个由'1'(陆地)和'0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
  解题思路 这个题我用并查集做的,算法设计的不大好,遍历了两次数组。这里的每个网格(矩阵)上的点都是一个类,所以在初始化parent数组时,需要将二维网格上点的位置与parent数组上的位置一一对应,就是矩阵按行优先的方式转换成一维数组,具体可以参考这篇博客;然后遍历矩阵,将相邻的'1'归并到一起;最后统计每个集合的个数,就是找每个集合的 B O S S BOSS ,如果网格点对应parent数组的 i n d e x index 与它所属的集合号码一样,那该点就是集合的 B O S S 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

3. LeetCode990:等式方程的可满足性

  LeetCode第990题:等式方程的可满足性
  题目 给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程equations[i]的长度为4,并采用两种不同的形式之一:"a==b""a!=b"。在这里, a a 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数组换成一个字典,应该是可行的,有时间再试试。

结束语

  通过这几个题可以发现,并查集类的题目大致思路都一样,就是根据题目建立集合,然后将有连接的集合进行归并。我觉得难点就是集合归并后,如何选择合适的方法去处理它来得到正确的结果,这个方法的选择貌似就决定了算法的复杂性,继续加油(ง •̀_•́)ง

猜你喜欢

转载自blog.csdn.net/qq_42730750/article/details/108305056
今日推荐