算法笔记:并查集

参考资料:

导引问题

在某个城市里住着 n n n 个人,现在给定这 n n n 个人的 m m m 条信息(每条信息格式【A B】,表示A和B认识)。
假设所有认识的人一定属于同一个单位,请计算该城市最多有多少单位?

什么是并查集?

Disjoint Set,即“不相交的集合”。

  • 问题描述:将编号分别为 1... N 1...N 1...N N N N 个对象划分为不相交的集合,在每个集合中,选择其中某个元素代表所在集合。

  • 常见两种操作

    • 两个集合

    • 找某元素属于哪个集合

实现方法1:数组

  • 用编号最小的元素标记所在集合;
  • 定义一个数组 Set[1, ..., n],其中 Set[i] 表示元素 i所在的集合。

在这里插入图片描述

即:数组的下标 i 表示元素(学生),下标i所对应的值 Set[i] 表示该元素所属的集合(班级)。一个集合的代表(班长)是该集合中的最小下标。

参考实现代码(Python)
class DisjointSet:
    '''数组实现,下标表示元素,值表示所属集合;
    用集合中的最小下标表示该集合。
    '''

    def __init__(self, dset=[]):
        self.dset = dset
    
    def find(self, x):
        '''查找元素x所属的集合'''
        return self.dset[x] 
    
    def union(self, A, B):
        '''合并集合A和B'''
        A, B = min(A, B), max(A, B)
        for i in range(len(self.dset)):
            if self.dset[i] == B:
                self.dset[i] = A
        return
        

# example    
ds = DisjointSet(dset=[1,2,1,4,2,6,1,6,2,2])
print(ds.find(0)) # 1
ds.union(1,2)
print(ds.dset) # [1, 1, 1, 4, 1, 6, 1, 6, 1, 1]
效率分析
  1. 查找: O ( 1 ) O(1) O(1),很高效!

  2. 合并: O ( N ) O(N) O(N),必须搜索全部元素,有待改进!

    • 例如合并A班和B班,则需要遍历整个数组,找出属于B班的学生,将其班长改为A的。

※ 该实现方法太过于扁平化,相当于班长直接管理每一位同学。
下面考虑树结构

实现方法2:树

  • 每个集合用一棵“有根树”表示:
    • 定义数组Set[1, ..., n]
      • Set[i] = i,则i表示本集合,并且是集合对应树的根(老大);
      • Set[i] = j,若 j ≠ i, 则 ji 的父节点(直属领导)。

在这里插入图片描述

参考实现代码(Python)

※ 怎么判断根节点:每个人心里装的都是领导,只有最大的boss心里装的只有自己。

class DisjointSet:
    '''数组实现的树结构,下标表示元素,值表示父节点;
    用树的根节点代表该集合。
    '''        
    def __init__(self, dset=[]):
        self.dset = dset
        
    def find(self, x):
        '''查找元素x所属的集合'''
        r = x
        while self.dset[r] != r:
            r = self.dset[r]
        return r 
    
    def union(self, A, B):
        '''合并集合A和B'''
        self.dset[B] = A
        return
        

ds = DisjointSet(dset=[0,1,2,1,0,2,1,2,2,1])
print(ds.find(0)) # 1
ds.union(1,0)
print(ds.dset) # [1,1,2,1,0,2,1,2,2,1]
效率分析
  1. 查找:最坏情况 O ( N ) O(N) O(N)(单链条形式),一般情况是 O ( log ⁡ N ) O(\log N) O(logN)

  2. 合并: O ( 1 ) O(1) O(1)

优化

如何避免最坏情况?

  • 方法:将深度小的树 合并到 深度大的树

  • 实现:假设两棵树的深度分别为 h 1 h_1 h1 h 2 h_2 h2,则合并后的树的高度 h h h 为:
    h = { max ⁡ ( h 1 , h 2 ) ,   if  h 1 ≠ h 2 h 1 + 1 ,   if  h 1 = h 2 h = \left\{ \begin{aligned} & \max(h_1, h_2), \ & \text{if} \ h_1 \neq h_2 \\ & h_1 + 1, \ & \text{if} \ h_1 = h_2 \end{aligned} \right. h={max(h1,h2), h1+1, if h1=h2if h1=h2

  • 效果:任意顺序的合并操作以后,包含 k k k 个节点的树的最大高度不超过 ⌊ lg ⁡ k ⌋ \lfloor \lg k \rfloor lgk

class DisjointSet:
    '''数组实现的树结构,下标表示元素,值表示父节点;
    用树的根节点代表该集合。
    '''        
    def __init__(self, dset=[]):
        self.dset = dset
        
    def find(self, x):
        '''查找元素x所属的集合'''
        r = x
        while self.dset[r] != r:
            r = self.dset[r]
        return r 
    
    def union(self, A, B):
        '''合并集合A和B'''
        if A.height == B.height: # TODO 需要记录节点的高度 
            A.height += 1
            self.dset[B] = A
        elif A.height < B.height:
            self.dset[A] = B
        else:
            self.dset[B] = A
        return 
  • 优化后查找的复杂度最坏为 O ( log ⁡ N ) O(\log N) O(logN)

例题1

题目描述

某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。现在的目标是使全省任何两个城镇间都可以实现交通(不一定要有直接的道路相连,只要有道路可达即可),问最少还需要建设多少条道路?

题目来源:浙江大学-研究生复试(机考)(好像是2005年的?)

解题思路

  • 就是要把所有城镇合并成一个集合!(n个城镇,最少需要n-1条路能实现全部连通;n个集合,最少需要新增n-1条路能实现全部连通)
  • 怎么判断一个并查集有多少个集合?——数根节点的个数:父节点等于自身的节点的个数。

※ 最赤裸裸的并查集问题。

在这里插入图片描述

初始化的时候,每个点都是一个集合;
每来一条边,判断这两个点是不是属于同一个集合;如果不是就合并。
最后返回根节点的个数-1

例题2

题目描述

判断一个迷宫是否符合要求:任何两个房间之间有且只有一条通路。

在这里插入图片描述

——说明要连通(只能有一个根节点),且不能出现环。

※ 如果两个点已经连通了,再给他们俩加条边,就会出现环

经典应用——最小生成树

什么是——生成树?

  • :由顶点和边组成,任何两个顶点之间都可以有边
  • :n个顶点n-1条边,并且是连通的
  • 生成树:如果一个图包含了某个图的所有顶点,并且包含了其中的n-1条边,并且是连通的
  • 最小生成树:边的权值和最小

在这里插入图片描述
在这里插入图片描述

  • 应用:用最小的代价让6个岛互连互通

如何求——最小生成树?

  1. Prim算法
  2. Kruskal算法——并查集

Kruskal算法

克鲁斯卡尔算法

理论基础:MST性质(最小生成树性质)

  • 对于一个连通图,至少存在一棵最小生成树,包含了最短的边。

  • 反证法:假设所有的最小生成树都不包含最短的那条边,…

算法步骤

  1. 把原始图的 N N N 个节点看成 N N N 个独立子图;
  2. 每次选取当前最短的边,看两端是否属于不同的子图(保证不会构成回路);若是,加入;否则,放弃;
  3. 循环操作步骤2,直到有 N − 1 N-1 N1 条边。

※ 典型的贪心算法

例题3

题目描述

地图上有 n n n 个城市,现在想给这 n n n 个城市之间造路,希望能让城市之间两两可达。给出了 m m m 种供选择的道路,每种选择是一个三元组 (u, v, w) ,代表给 u 城市和 v 城市之间建造一条长度为 w 的道路。

要求最终的道路总长越小越好。

解题思路

  • 要连通,要总长最小——就是最直白的最小生成树

765. 情侣牵手

题目描述

(困难)N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手。 计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。 一次交换可选择任意两人,让他们站起来交换座位。

人和座位用 02N-1 的整数表示,情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2N-2, 2N-1)

这些情侣的初始座位 row[i] 是由最初始坐在第 i 个座位上的人决定的。

相关标签:贪心算法,并查集,图

解题思路

  • 「首尾相连」这件事情可以使用 并查集 表示,将输入数组相邻位置的两个 编号 在并查集中进行合并。编写代码基于了下面的事实:

    如果一对情侣恰好坐在了一起,并且坐在了成组的座位上,其中一个下标一定是偶数,另一个一定是奇数,并且「偶数的值 + 1 = 奇数的值」。例如编号数对 [2, 3][9, 8],这些数对的特点是除以 2(下取整)得到的数相等。

  • 并查集:交换之后连通分量个数 - 交换之前连通分量个数 = 最少交换次数

在这里插入图片描述

我的题解

def minSwapsCouples(row):
    """
    :type row: List[int]
    :rtype: int
    """
    N = len(row) // 2
    
    # 构造并查集:数组实现树结构
    # 代表元法:用集合中的最小编号作为该集合的代表
    
    # 初始化:每对情侣属于一个集合,编号满足a//2==b//2的就是情侣
    # 即初始化为[0,0,2,2,4,4,...,2N-1,2N]
    # disjoint_set的下标表示每个人的编号
    disjoint_set = [i//2*2 for i in range(2*N)]
    
    def find(x):
        '查找编号为x的人所属的集合'
        r = x
        while r != disjoint_set[r]:
            r = disjoint_set[r]
        return r
    
    def union(a, b):
        '将编号为a和b的人所属的集合合并在一起'
        disjoint_set[a] = b
        return
    
    # 坐在一起的人join到一个集合中
    # 例如0和4坐在一起(在row中相邻),则将4归属到0的集合中 
    for i in range(0, 2*N, 2):
        p1, p2 = find(row[i]), find(row[i+1]) #找到所属的集合
        union(p1, p2) #合并
            
    # 统计集合的个数,即disjoint_set中根节点的数量
    count = 0
    for i in range(2*N):
        if i==disjoint_set[i]: # 父节点等于自身的就是根节点
            count += 1
    # 最少交换次数 = N - 连通集合的个数
    return N-count


print(minSwapsCouples(row = [0, 2, 1, 3])) # 1
print(minSwapsCouples(row = [3, 2, 0, 1])) # 0
print(minSwapsCouples(row = [5,4,2,6,3,1,0,7])) # 2      
print(minSwapsCouples(row = [6,2,1,7,4,5,3,8,0,9])) # 3     

复杂度分析:

  • 时间复杂度: O ( N log ⁡ N ) O(N \log N) O(NlogN),这里 N N N 是输入数组的长度, O ( N 2 log ⁡ N 2 ) = O ( N log ⁡ N ) O(\cfrac{N}{2} \log \cfrac{N}{2}) = O(N\log N) O(2Nlog2N)=O(NlogN) ;查找函数find()的复杂度是 O ( log ⁡ N ) O(\log N) O(logN)
  • 空间复杂度: O ( N ) O(N) O(N)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值