美文网首页
并查集(Union-find set) 2020-04-08(未

并查集(Union-find set) 2020-04-08(未

作者: 9_SooHyun | 来源:发表于2020-04-08 11:56 被阅读0次

    啥是并查集(Union-find set)

    并查集是用于合并集合的一种树形的逻辑数据结构,实际上底层通过【数组】实现。它只能合并集合,不能分割集合

    并查集要解决的问题

    解决【需要通过某个元素查找该元素所在的集合】的问题
    当需要【通过某个元素查找该元素所在的集合】时,我们有什么办法呢?首先一个,可以给每个元素带上一个属性,该属性标记该元素归属于哪个集合;另外一个,我们可以把【通过属性记录集合所属关系的方式优化成根据元素搜索集合的根root】,就是并查集方法。

    并查集算法思想

    每个集合内部应该按照树形结构组织,这样每个集合就是一棵树,就可以由树根root唯一代表,所有集合就组成一片森林。例如,每个公司就是个树形的集合,根root就是老板,马云可以拿出来代表阿里集合,Pony可以代表腾讯集合。如果我们想合并两个集合,只需要将一个root作为另一个root的孩子即可,这就是树形结构的优势。具体步骤如下:

    • 1.每个元素自成一个集合,每个元素本身就是root
    • 2.集合合并时,集合根结点间重新形成上下级关系即可

    并查集由一个数组或者字典father和两个方法findroot()和union()构成
    father记录每个点的父节点,如 num_a = father[num_b],表示num_a是num_b的father
    函数findroot()接收一个num,查找这个num所在集合的根爸爸;
    union()接收两个num(num_A, num_B),查找num_A, num_B所在集合的根爸爸,通过根爸爸相连实现两个集合的union

    代码模板

    class UnionFindSet:
        def __init__(self, father=[]):
            self.father = father
            
    
        # 查找并返回ele所在集合的根爸爸root
        def findRoot(self, ele):
            root = ele
            
            # 当自己不是自己的father,说明就还不是root,继续向上搜索集合树
            while root != self.father[root]:
                root = self.father[root]
            return root
        
        # 合并两个集合
        def union(self, ele1, ele2):
        
            # 分别查找两个元素所在集合的根爸爸root
            root1 = self.findRoot(ele1)
            root2 = self.findRoot(ele2)
            
            # 如果两个元素不在同一集合,则合并两个集合
            if root1 != root2:
                self.father[root1] = root2
    

    优化:
    我们已经知道每个集合是一棵树。对于集合中的元素ele,要查找所在集合的root,必须层层向上查找,查找次数为树的高度。极端情况下,如果树只存在一条路径,则退化成了线性查找。显然的思路是,我们让每棵集合树变得尽可能矮胖,最矮矮到什么层度?2层高的树,不能再矮了


    并查集压缩树高度

    于是,我们在findRoot中做一些优化,利用递归将整个查找路径上的所有元素的father都修改为root:

    # 查找并返回ele所在集合的根爸爸root
        def findRoot(self, ele):
            # 利用递归不断更新一条路径上的father,并减小了集合内树的高度
            if self.father[ele] != ele:
                self.father[ele] = self.findRoot(self.father[ele])    
            return self.father[ele] 
    

    于是,并查集类的代码模板如下:

    class UnionFindSet:
        def __init__(self, father=[]):
            self.father = father
            
        # 查找并返回ele所在集合的根爸爸root
        def findRoot(self, ele):
            # 利用递归不断更新一条路径上的father,并减小了集合内树的高度
            if self.father[ele] != ele:
                self.father[ele] = self.findRoot(self.father[ele])    
            return self.father[ele]
        
        # 合并两个集合
        def union(self, ele1, ele2):
            # 分别查找两个元素所在集合的根爸爸root
            root1 = self.findRoot(ele1)
            root2 = self.findRoot(ele2)
            
            # 如果两个元素不在同一集合,则合并两个集合
            if root1 != root2:
                self.father[root1] = root2
    

    并查集的应用

    leetcode990. 等式方程的可满足性

    给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
    只有当可以满足所有给定的方程时才返回 true,否则返回 false。

    分析:
    题目中存在两种表达式,分别描述等量关系和不等量关系。我们可以先利用等量关系,将相等的元素划分入同一个集合内;集合建设完毕后,再通过不等关系,检查是否和集合自相矛盾,以判断全体式子是否能够被满足。

    思路:
    首先处理所有等式,将相等的元素纳入同一集合,需要查找等式中元素归属的集合进行集合合并,因此可以选择给元素带上归属属性,也可以通过并查集实现

    • 通过设置属性查找归属集合的代码
    class Solution:
        def equationsPossible(self, equations: List[str]) -> bool:
            # 考虑将相等元素放入一个集合中
            # 【先将equations排序,==在前,!=在后。先处理相等的情况,划分好集合,此时只负责建好集合,不判断是否合法;然后再处理不等关系,通过不等关系判断建好的集合是否valid。】
            equations.sort(key=lambda x: x[1:3], reverse=True)
            # print(equations)
            l = len(equations)
            # 每个元素归属集合的属性,通过字典记录
            varies_to_set_Dict = dict()
            set_no = 0
            for i in range(l):
                # 处理每一个表达式
    
                exp = equations[i]
                ele1, ele2, ops = exp[0], exp[-1], exp[1:3]
    
                # 如果两个变量都出现在已有集合中
                if ele1 in varies_to_set_Dict and ele2 in varies_to_set_Dict:
                    # 将ele1和ele2所在的两个集合合并,归到最先产生的集合上,即set_no小的集合上
                    if ops == '==':
                        s1, s2 = varies_to_set_Dict[ele1], varies_to_set_Dict[ele2]
                        if s1 < s2:
                            # s2集合中的元素迁移到s1,实现合并
                            for set_ele in locals()[s2]:
                                varies_to_set_Dict[set_ele] = s1
                        else:
                            # s1集合中的元素迁移到s2,实现合并
                            for set_ele in locals()[s1]:
                                varies_to_set_Dict[set_ele] = s2
                    else:
                        if varies_to_set_Dict[ele1] == varies_to_set_Dict[ele2]:
                            return False
                # 如果两个变量都没出现在已有集合中,创建集合并纳入元素
                elif ele1 not in varies_to_set_Dict and ele2 not in varies_to_set_Dict:
                    if ops == '==':
                        set_name = 'set_%s' % str(set_no)
                        locals()[set_name] = set()
                        locals()[set_name].add(ele1)
                        locals()[set_name].add(ele2)
                        set_no += 1
                        # 登记所属集合的标记
                        varies_to_set_Dict[ele1] = set_name
                        varies_to_set_Dict[ele2] = set_name
                    else:
                        if ele1 == ele2:
                            return False
                        set_name = 'set_%s' % str(set_no)
                        locals()[set_name] = set()
                        locals()[set_name].add(ele1)
                        # 登记所属集合的标记
                        varies_to_set_Dict[ele1] = set_name
                        set_no += 1
                        set_name = 'set_%s' % str(set_no)
                        locals()[set_name] = set()
                        locals()[set_name].add(ele2)
                        # 登记所属集合的标记
                        varies_to_set_Dict[ele2] = set_name
                        set_no += 1
                # 只有一个变量出现在已有集合中
                else:
                    # 只关注相等关系
                    if ops == '==':
                        if ele1 in varies_to_set_Dict:
                            varies_to_set_Dict[ele2] = varies_to_set_Dict[ele1]
                            locals()[varies_to_set_Dict[ele2]].add(ele2)
                        else:
                            varies_to_set_Dict[ele1] = varies_to_set_Dict[ele2]    
                            locals()[varies_to_set_Dict[ele1]].add(ele1)
                # print(varies_to_set_Dict)
            return True
    
    • 通过并查集查找归属集合的代码
    class Solution:
        def equationsPossible(self, equations: List[str]) -> bool:
            # 考虑将相等元素放入一个集合中
            # 【先将equations排序,==在前,!=在后。先处理相等的情况,划分好集合,此时只负责建好集合,不判断是否合法;然后再处理不等关系,通过不等关系判断建好的集合是否valid。】
            equations.sort(key=lambda x: x[1:3], reverse=True)
            # print(equations)
            l = len(equations)
            father = dict()
            ufs = UnionFindSet(father)
            for i in range(l):
                # 处理每一个表达式
    
                exp = equations[i]
                ele1, ele2, ops = exp[0], exp[-1], exp[1:3]
                # 找集合根
                r1, r2 = ufs.findRoot(ele1), ufs.findRoot(ele2)
                if ops == '==':
                    # 合并
                    ufs.union(r1, r2)
                else:
                    if r1 == r2:
                        return False
                    
            return True
    
    class UnionFindSet:
        def __init__(self, father=dict()):
            self.father = father
            
    
        # 查找并返回ele所在集合的根爸爸root
        def findRoot(self, ele):
            root = ele
            
            # 如果ele没有father,自己做自己的father,返回
            if root not in self.father:
                self.father[root] = root
                return root
            
            # 当自己不是自己的father,说明就还不是root,继续沿路径向上搜索
            if root != self.father[root]:
                self.father[root] = self.findRoot(self.father[root])
            return self.father[root]
        
        # 合并两个集合
        def union(self, ele1, ele2):
            # 分别查找两个元素所在集合的根爸爸root
            root1 = self.findRoot(ele1)
            root2 = self.findRoot(ele2)
            
            # 如果两个元素不在同一集合,则合并两个集合
            if root1 != root2:
                self.father[root1] = root2
    

    leetcode #### 785. 判断二分图
    本题的切入点在于,node i 与 graph[i]的任一元素不在同一集合,而graph[i]内的全部元素在同一集合。期间涉及到集合的合并,这里提供本人使用并查集的golang版本答案

    //// 实现并查集 ////
    
    type UFS struct {
        father_slice []int
    }
    // findRoot和union方法绑定UFS struct
    
    // 查找node在并查集中的root
    func (ufs UFS) findRoot(node int) int {
        if ufs.father_slice[node] != node {
            // 让node的father直接指向根root
            ufs.father_slice[node] = ufs.findRoot(ufs.father_slice[node])
        }
        return ufs.father_slice[node]
    }
    
    // union合并
    func (ufs UFS) union(node1, node2 int) {
        node1_root, node2_root := ufs.findRoot(node1), ufs.findRoot(node2)
        if node1_root != node2_root {
            // root2做root1的小弟
            ufs.father_slice[node2_root] = node1_root
        }
    }
    
    //// 实现并查集 ////
    
    func isBipartite(graph [][]int) bool {
        ufs := UFS{make([]int, len(graph))}
        for i := 0; i < len(graph); i ++ {
            ufs.father_slice[i] = i
        }
    
        // 每个i与graph[i]属于互斥集合
        for i := 0; i < len(graph); i ++ {
            // i与graph[i]的元素v属于一个集合,false
            for _, v := range graph[i] {
                if ufs.findRoot(i) == ufs.findRoot(v) {
                    return false
                }
            }
            
            // 否则,graph[i]元素合并
            if len(graph[i]) > 0 {
                first_ele := graph[i][0]
                for j := 1; j < len(graph[i]); j ++ {
                    ufs.union(first_ele, graph[i][j])
                }
            }
        }
        fmt.Println(ufs.father_slice)
        return true
    }
    

    相关文章

      网友评论

          本文标题:并查集(Union-find set) 2020-04-08(未

          本文链接:https://www.haomeiwen.com/subject/pwmzphtx.html