并查集模板——核心就是路径压缩

2023/2/2更新:

补充一个二维矩阵并查集的leetcode题目

130. 被围绕的区域

难度中等
给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

 

示例 1:

输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

示例 2:

输入:board = [["X"]]
输出:[["X"]]

当然,使用dfs也可以做!

class Solution:
    def solve(self, board) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        m, n = len(board), len(board[0])

        def need_fill(x, y):
            # print("seen:", seen)
            board[x][y] = '#'            
            for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                x2, y2 = x + dx, y + dy
                if 0 <= x2 < m and 0 <= y2 < n and (x2, y2) and board[x2][y2] == 'O':
                    need_fill(x2, y2)

        for j in range(n):
            if board[0][j] == 'O':
                need_fill(0, j)
            if board[m - 1][j] == 'O':
                need_fill(m - 1, j)

        for i in range(1, m - 1):
            if board[i][0] == 'O':
                need_fill(i, 0)
            if board[i][n - 1] == 'O':
                need_fill(i, n - 1)

        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O':
                    board[i][j] = 'X'
                elif board[i][j] == '#':
                    board[i][j] = 'O'

        return board

  



我们用并查集写下,杜绝了繁琐的判定seen过程,将临界边上的O和其连接的O统统归到dummy node上:
class UionFind:
    def __init__(self, cnt):
        self.parent = {i:i for i in range(cnt)}

    def union(self, i, j):
        self.parent[self.find(i)] = self.find(j)

    def find(self, i):
        path = []
        node = i
        while node != self.parent[node]:
            path.append(node)
            node = self.parent[node]

        root = node
        for node in path:
            self.parent[node] = root
        return root

    def is_connect(self, i, j):
        return self.find(i) == self.find(j)


class Solution:
    def solve(self, board) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        m, n = len(board), len(board[0])
        uf = UionFind(m*n + 1)
        dummy = m*n

        def node(i, j):
            return i*n + j

        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O':
                    if i == 0 or i == m-1 or j == 0 or j == n-1:
                        uf.union(node(i,j), dummy)

                    for dx, dy in [(1, 0), (0, 1)]:
                        i2, j2 = i + dx, j + dy
                        if i2 < m and j2 < n and board[i2][j2] == 'O':
                            uf.union(node(i, j), node(i2, j2))

        for i in range(m):
            for j in range(n):
                if board[i][j] == 'O' and not uf.is_connect(node(i, j), dummy):
                    board[i][j] = 'X'

        return board

  ---------以上为更新

 

547. 朋友圈

难度中等

班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

示例 1:

输入: 
[[1,1,0],
 [1,1,0],
 [0,0,1]]
输出: 2 
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。

示例 2:

输入: 
[[1,1,0],
 [1,1,1],
 [0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。

注意:

  1. N 在[1,200]的范围内。
  2. 对于所有学生,有M[i][i] = 1。
  3. 如果有M[i][j] = 1,则有M[j][i] = 1。

 

class Solution(object):
    def findCircleNum(self, M):
        """
        :type M: List[List[int]]
        :rtype: int
        """
        N = len(M)
        self.init(N)
        for i in range(0, N):
            for j in range(i + 1, N):
                if M[i][j]:
                    self.connect(i, j)

        return self.count()

    def count(self):
        cnt = 0
        for i,n in enumerate(self.father):
            if self.father[i] == i:
                cnt += 1
        return cnt

    def init(self, N):
        self.father = [0] * N
        for i in range(N):
            self.father[i] = i

    def connect(self, a, b):
        self.father[self.find(a)] = self.find(b)

    def find(self, node):
        path = []
        while self.father[node] != node:
            path.append(node)
            node = self.father[node]

        for n in path:
            self.father[n] = node

        return node

 

684. 冗余连接

难度中等

在本问题中, 树指的是一个连通且无环的无向图。

输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。

结果图是一个以组成的二维数组。每一个的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。

返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v

示例 1:

输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
  1
 / \
2 - 3

示例 2:

输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
    |   |
    4 - 3

注意:

  • 输入的二维数组大小在 3 到 1000。
  • 二维数组中的整数在1到N之间,其中N是输入数组的大小。

更新(2017-09-26):
我们已经重新检查了问题描述及测试用例,明确图是无向 图。对于有向图详见冗余连接II。对于造成任何不便,我们深感歉意。

 
class Solution(object):
    def findRedundantConnection(self, edges):
        """
        :type edges: List[List[int]]
        :rtype: List[int]
        """
        N = len(edges)
        self.init(N)
        ans = [None, None]
        for i,j in edges:
            is_success, same_nodes = self.connect(i, j)
            if not is_success:
                ans = same_nodes
        return ans

    def connect(self, a, b):
        f1 = self.find(a)
        f2 = self.find(b)
        if f1 != f2:
            self.father[f1] = f2
            return True, [None,None]
        else:
            return False, [a,b]

    def init(self, N):
        self.father = {}
        for i in range(1, N+1):
            self.father[i] = i
    
    def find(self, node):
        path = []
        while self.father[node] != node:
            path.append(node)
            node = self.father[node]
            
        for n in path:
            self.father[n] = node
            
        return node

  

 

591. 连接图 III

中文
English

给一个图中的 n 个节点, 记为 1n . 在开始的时候图中没有边.
你需要完成下面两个方法:

  1. connect(a, b), 添加一条连接节点 a, b的边
  2. query(), 返回图中联通区域个数

样例

例1:

输入:
ConnectingGraph3(5)
query()
connect(1, 2)
query()
connect(2, 4)
query()
connect(1, 4)
query()

输出:[5,4,3,3]

例2:

输入:
ConnectingGraph3(6)
query()
query()
query()
query()
query()


输出:
[6,6,6,6,6]
class ConnectingGraph3:
    """
    @param a: An integer
    @param b: An integer
    @return: nothing
    """
    def __init__(self, n):
        # initialize your data structure here.
        self.father = [0]*(n+1)
        self.n = n
        for i in range(1, n+1):
            self.father[i] = i
        self.zone_cnt = n
    
    def find(self, p):
        paths = []
        while p != self.father[p]:
            paths.append(p)
            p = self.father[p]
        root = p
        
        for node in paths:
            self.father[node] = root
        
        return root
        
    
    def connect(self, a, b):
        # write your code here
        f1 = self.find(a)
        f2 = self.find(b)
        
        if f1 != f2:
            self.zone_cnt -= 1
            self.father[f1] = f2
        
    """
    @return: An integer
    """
    def query(self):
        # write your code here
        return self.zone_cnt

 当然,这个题目使用BFS也是可以的!我在面试一个小伙的时候他就这么做过。

class ConnectingGraph3:
    """
    @param a: An integer
    @param b: An integer
    @return: nothing
    """
    def __init__(self, n):
        # initialize your data structure here.
        self.seen = set()
        self.node_map = {}
        self.n = n
    
    def connect(self, a, b):
        # write your code here
        if a not in self.node_map:
            self.node_map[a] = set()

        self.node_map[a].add(b)
        
        if b not in self.node_map:
            self.node_map[b] = set()
        
        self.node_map[b].add(a)
        
    """
    @return: An integer
    """
    def query(self):
        # write your code here
        ans = 0
        
        self.seen = set()
        for i in range(1, self.n+1):
            if i not in self.seen:
                self.bfs(i)
                ans += 1
        
        return ans
    
    def bfs(self, root):
        q = collections.deque([root])
        self.seen.add(root)
        
        while q:
            node = q.popleft()
            
            if node not in self.node_map:
                continue
            
            for neighbor in self.node_map[node]:
                if neighbor not in self.seen:
                    self.seen.add(neighbor)
                    q.append(neighbor)
        
        

 

434. 岛屿的个数II

中文
English

给定 n, m, 分别代表一个二维矩阵的行数和列数, 并给定一个大小为 k 的二元数组A. 初始二维矩阵全0. 二元数组A内的k个元素代表k次操作, 设第i个元素为 (A[i].x, A[i].y), 表示把二维矩阵中下标为A[i].x行A[i].y列的元素由海洋变为岛屿. 问在每次操作之后, 二维矩阵中岛屿的数量. 你需要返回一个大小为k的数组.

样例

样例 1:

输入: n = 4, m = 5, A = [[1,1],[0,1],[3,3],[3,4]]
输出: [1,1,2,2]
解释: 
0.  00000
    00000
    00000
    00000
1.  00000
    01000
    00000
    00000
2.  01000
    01000
    00000
    00000
3.  01000
    01000
    00000
    00010
4.  01000
    01000
    00000
    00011

样例 2:

输入: n = 3, m = 3, A = [[0,0],[0,1],[2,2],[2,1]]
输出: [1,1,2,2]

注意事项

设定0表示海洋, 1代表岛屿, 并且上下左右相邻的1为同一个岛屿.

 

这个题目有点难,主要是要通过100%的用例还是不好搞,是GG的。。。

"""
Definition for a point.
class Point:
    def __init__(self, a=0, b=0):
        self.x = a
        self.y = b
"""


class Solution:
    """
    @param n: An integer
    @param m: An integer
    @param operators: an array of point
    @return: an integer array
    """

    def numIslands2(self, n, m, operators):
        # write your code here
        self.father = {}

        self.matrix = [[0] * m for i in range(n)]
        for i in range(n):
            for j in range(m):
                self.father[(i, j)] = (i, j)

        ans = []

        self.cnt = 0
        for p in operators:
            a, b = p.x, p.y
            if self.matrix[a][b]:
                ans.append(self.cnt)
                continue
            
            self.matrix[a][b] = 1
            self.cnt += 1
            
            for x, y in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                if 0 <= a + x < n and 0 <= b + y < m and self.matrix[a + x][b + y]:
                    f1 = self.find(a + x, b + y)
                    f2 = self.find(a, b)
                    if f1 != f2:
                        self.father[f1] = f2
                        self.cnt -= 1
            
            ans.append(self.cnt)

        return ans

    def find(self, p1, p2):
        a, b = p1, p2
        path = []
        while (a, b) != self.father[(a, b)]:
            path.append((a, b))
            a, b = self.father[(a, b)]

        for p in path:
            self.father[p] = (a, b)

        return (a, b)

 主要是细节处理,比较容易出错!!!

对于每一次操作(x, y), 如果(x, y)的上下左右都是0, 那么计数器加一; 如果不全为0, 则:

  1. 并查集查询其四周的1所属的集合, 假设它们属于 k 个不同的集合
  2. 计数器减去 k-1
  3. 将这 k 个集合, 连同 (x, y), 合并

178. 图是否是树

中文
English

给出 n 个节点,标号分别从 0n - 1 并且给出一个 无向 边的列表 (给出每条边的两个顶点), 写一个函数去判断这张`无向`图是否是一棵树

样例

样例 1:

输入: n = 5 edges = [[0, 1], [0, 2], [0, 3], [1, 4]]
输出: true.

样例 2:

输入: n = 5 edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]
输出: false.

注意事项

你可以假设我们不会给出重复的边在边的列表当中. 无向边 [0, 1][1, 0] 是同一条边, 因此他们不会同时出现在我们给你的边的列表当中。

class Solution:
    """
    @param n: An integer
    @param edges: a list of undirected edges
    @return: true if it's a valid tree, or false
    """
    def validTree(self, n, edges):
        if n - 1 != len(edges): # 这个是必须的,因为tree最终只能是n-1条变
            return False
        
        self.father = {i: i for i in range(n)}
        self.size = n
        
        for a, b in edges:
            self.union(a, b)
            
        return self.size == 1 #最终只能是一个孤岛
        
    def union(self, a, b):
        root_a = self.find(a)
        root_b = self.find(b)
        if root_a != root_b:
            self.size -= 1
            self.father[root_a] = root_b
        
    def find(self, node):
        path = []
        while node != self.father[node]:
            path.append(node)
            node = self.father[node]
            
        for n in path:
            self.father[n] = node
            
        return node

 

1070. 账户合并

中文
English

给定一个帐户列表,每个元素accounts [i]是一个字符串列表,其中第一个元素accounts [i] [0]是账户名称,其余元素是这个帐户的电子邮件。
现在,我们想合并这些帐户。
如果两个帐户有相同的电子邮件地址,则这两个帐户肯定属于同一个人。
请注意,即使两个帐户具有相同的名称,它们也可能属于不同的人,因为两个不同的人可能会使用相同的名称。
一个人可以拥有任意数量的账户,但他的所有帐户肯定具有相同的名称。
合并帐户后,按以下格式返回帐户:每个帐户的第一个元素是名称,其余元素是按字典序排序后的电子邮件。
帐户本身可以按任何顺序返回。

样例

样例 1:
	输入:
	[
		["John", "johnsmith@mail.com", "john00@mail.com"],
		["John", "johnnybravo@mail.com"],
		["John", "johnsmith@mail.com", "john_newyork@mail.com"],
		["Mary", "mary@mail.com"]
	]
	
	输出: 
	[
		["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],
		["John", "johnnybravo@mail.com"],
		["Mary", "mary@mail.com"]
	]

	解释: 
	第一个第三个John是同一个人的账户,因为这两个账户有相同的邮箱:"johnsmith@mail.com".
	剩下的两个账户分别是不同的人。因为他们没有和别的账户有相同的邮箱。

	你可以以任意顺序返回结果。比如:
	
	[
		['Mary', 'mary@mail.com'],
		['John', 'johnnybravo@mail.com'],
		['John', 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com']
	]
	也是可以的。

注意事项

账户个数在1~1000之间
每个账户下的电子邮件在1~10之间
每个字符串的长度在1~30之间

class Solution:
    """
    @param accounts: List[List[str]]
    @return: return a List[List[str]]
    """
    def accountsMerge(self, accounts):
        # write your code here
        acc_dict = self.init(accounts)
        
        clusters = self.join_accounts(acc_dict)
        
        return self.merge(clusters, acc_dict)
        
    def init(self, accounts):
        acc_dict = {}
        self.father = {}
        for acc in accounts:
            for i in range(1, len(acc)):
                if acc[i] not in self.father: 
                    self.father[acc[i]] = acc[1]
                else: # detail CAUSTION
                    f1 = self.find(acc[i])
                    f2 = self.find(acc[1])
                    self.father[f1] = f2
                    
                acc_dict[acc[i]] = acc[0]
                
        return acc_dict
    
    def find(self, x):
        path = []
        while x != self.father[x]:
            path.append(x)
            x = self.father[x]
        
        for p in path:
            self.father[p] = x
        
        return x
    
    def join_accounts(self, acc_dict):
        clusters = collections.defaultdict(set)
        for acc in acc_dict:
            f = self.find(acc)
            clusters[f].add(acc)
        
        return clusters
    
    def merge(self, clusters, acc_dict):
        ans = []
        for c in clusters:
            ans.append([acc_dict[c]] + sorted(list(clusters[c])))
        
        return  ans
        

有没有发现这种题目比较XXX!小细节处理易错,一次性100%通过用例比较难!

我自己处理的逻辑不清晰,应该用下面的方式!!!

1396. 集合合并

中文
English

有一个集合组成的list,如果有两个集合有相同的元素,将他们合并。返回最后还剩下几个集合。

样例

样例1:

输入:list = [[1,2,3],[3,9,7],[4,5,10]]
输出:2 .
样例:剩下[1,2,3,9,7]和[4,5,10]这2个集合。

样例 2:

输入:list = [[1],[1,2,3],[4],[8,7,4,5]]
输出 :2
解释:剩下[1,2,3]和[4,5,7,8] 2个集合。

注意事项

  • 集合数 n <= 1000
  • 每个集合的元素个数 m <= 100
  • 元素一定是非负整数,且不大于 100000
class Solution:
    """
    @param sets: Initial set list
    @return: The final number of sets
    """
    def setUnion(self, sets):
        # Write your code here
        self.init(sets)
        self.connect(sets)
        return self.get_clusters()
        
    def init(self, sets):
        self.data_set = set()
        self.father = {}
        for items in sets:
            for x in items:
                self.father[x] = x
                self.data_set.add(x)

    def connect(self, sets):
        for items in sets:
            for i in range(1, len(items)):
                self.union(items[0], items[i]) # 关键!!!就按部就班来就好,不要去创新!!!
    
    def get_clusters(self):
        clusters = collections.defaultdict(set)
        for x in self.data_set:
            f = self.find(x)
            clusters[f].add(x)
        
        return len(clusters)
    
    def union(self, x, y):
        self.father[self.find(x)] = self.find(y)

    def find(self, x):
        path = []
        while x != self.father[x]:
            path.append(x)
            x = self.father[x]
        
        for p in path:
            self.father[p] = x
        
        return x

 非常清晰和简单的逻辑,写起来代码就非常不容易出错了!!!关键就是找到union!!!

 

 

posted @ 2020-04-03 19:13  bonelee  阅读(317)  评论(1编辑  收藏  举报