【算法实战】每日一题:18.1并查集知识点讲解以及算法实战

news2024/11/15 6:37:56

1.题目

给定一个序列,通过n-1次相邻元素的合并操作,恢复原始序列。

2.涉及知识点 - 并查集 (Union-Find)

并查集 (Union-Find) 详解

概述

并查集(Union-Find),也称为不相交集数据结构,用于处理一些不相交集合(Disjoint Sets)的合并(Union)及查询(Find)问题。并查集是一种高效的数据结构,常用于图论中的连通性问题,如判断两个元素是否属于同一个集合。

基本概念

  1. 代表元(代表元素):每个集合有一个特别的元素作为其代表元,集合的所有操作通过代表元来完成。(某个集合的一个代表。用于表示这个集合 )
  2. 合并操作(Union):将两个不同的集合合并为一个集合。
  3. 查找操作(Find):确定某个元素属于哪个集合,返回集合的代表元。

基本实现

并查集可以用两种方法来实现:数组实现树实现。通常,树实现更常用,因为它更高效(其实本身就是树形结构,数组实现和树实现只是一个称呼而已。)

数组实现

在数组实现中,并查集用一个简单的数组来表示,每个元素的值指向它所属集合的代表元素。(他爹,不是整个家族的爹)

特点
  • 简单:实现起来非常简单,直观地通过数组的索引来操作。
  • 效率低:在合并操作中,需要遍历整个数组来更新代表元素,时间复杂度为 (O(n))。
代码示例
class UnionFindArray:
    def __init__(self, n):
        self.parent = list(range(n))  # 初始化,每个元素的父节点指向自己

    def find(self, x):
        return self.parent[x]  # 查找操作,返回代表元

    def union(self, x, y):
        rootX = self.find(x)
        rootY = self.find(y)
        if rootX != rootY:
            for i in range(len(self.parent)):
                if self.parent[i] == rootY:
                    self.parent[i] = rootX  # 合并操作

这里实际上就find操作,只是找到当前元素的父亲,而不是整个集合的父亲。然后再进行合并操作的时候。他会把每一个元素他的父亲都给刷新一遍。所以效率是比较低的。

劣势
  • 合并操作效率低:每次合并需要遍历整个数组,导致时间复杂度较高。
  • 没有路径压缩:没有优化查找操作,会导致查找操作效率低下。

更好的方法是通过更新根节点来实现合并,而不是遍历整个数组。可以使用路径压缩和按秩合并来优化这个过程。(最大的区别)

下面是使用路径压缩和按秩合并的并查集实现,它在合并操作时只更新根节点的父节点,并保持树的平衡:

优化的并查集实现 - 树实现

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))  # 初始化,每个元素的父节点指向自己,索引表示父节点
        self.rank = [1] * n  # 初始化,每棵树的秩为1,每个位置都是一个节点,这里长度为n,每个都是1

    def find(self, x):
        if self.parent[x] != x: # 父节点不是自己的
            self.parent[x] = self.find(self.parent[x])  # 路径压缩(递归设置他爸)
        return self.parent[x] # 通过孩返回他爹

    def union(self, x, y):
        # 选取的两个元素他爹
        rootX = self.find(x)
        rootY = self.find(y)
        # 两爹不一样,表示不在一个集合里面,不是一家人
        if rootX != rootY:
            # 按秩合并
            # rootx家里辈分高(注意秩指的是树的高度),就把rooty以及家里人认rootx做爹
            # 父亲与父亲之间的较量,谁辈分小就得认另外一个做爹
            if self.rank[rootX] > self.rank[rootY]: 
                self.parent[rootY] = rootX
            elif self.rank[rootX] < self.rank[rootY]:
                self.parent[rootX] = rootY
            else:
                # 随意
                self.parent[rootY] = rootX
                self.rank[rootX] += 1  # 只有秩相同时才增加秩

树结构最大的区别就是,它用了这个递归去把每一个元素提前把父亲给找好了。这样的话,我们去找集合里面的根节点是就比较方便。

随便找这个集合里面的某一个元素就可以找到当前集合的根节点,而不是当前元素的父节点

秩操作解释

秩(Rank)在并查集中表示树的高度或者近似高度。按秩合并的基本思想是将高度较低的树连接到高度较高的树上,以保持树的整体高度尽量低,从而加快后续的查找操作。

秩的定义

在并查集中,秩可以定义为树的高度或者树中节点的最大深度。初始时,每个节点自己独立构成一个集合,其秩为 1。随着集合的合并,树的高度可能增加,我们通过秩来记录这种变化。

为什么秩相同时才增加秩

当我们合并两个集合时,有以下几种情况:

  1. 树的秩不同

    • 如果一棵树的秩大于另一棵树,我们将秩较小的树的根节点连接到秩较大的树的根节点上。
    • 这种情况下,合并后的树的高度不会增加,因为较小的树的节点都连接到较大的树中,不会影响较大的树的高度。
  2. 树的秩相同

    • 如果两棵树的秩相同,我们将其中一棵树的根节点连接到另一棵树的根节点上。
    • 这种情况下,合并后的树的高度会增加 1,因为两棵树的高度相同,连接后新的根节点的深度增加了1
      image-20240609205821380
  3. 路径压缩
    find 操作中,我们递归地找到根节点,并将路径上所有节点的父节点直接设为根节点。这减少了树的高度,加速了后续的操作。

  4. 按秩合并
    union 操作中,我们比较两个树的秩,将秩较小的树连接到秩较大的树上。如果两棵树的秩相同,我们将其中一棵树连接到另一棵树上,并增加根节点的秩。这保持了树的平衡,避免退化为链表

优化的合并操作的优势

  • 效率高:不需要遍历整个数组,只需要更新根节点的父节点。
  • 路径压缩:减少树的高度,加速后续的 find 操作。
  • 按秩合并:保持树的平衡,避免退化为链表。

示例使用

# 初始化并查集
uf = UnionFind(5)

# 合并操作
uf.union(0, 1)
uf.union(1, 2)

# 查找操作
print(uf.find(0))  # 输出: 0
print(uf.find(1))  # 输出: 0
print(uf.find(2))  # 输出: 0
print(uf.find(3))  # 输出: 3

# 合并操作
uf.union(3, 4)

# 查找操作
print(uf.find(3))  # 输出: 3
print(uf.find(4))  # 输出: 3

# 合并两个集合
uf.union(0, 3)

# 查找操作
print(uf.find(0))  # 输出: 0
print(uf.find(4))  # 输出: 0

适用于各种动态连通性问题。

并查集的应用

  1. 网络连通性:判断网络中的节点是否连通。
  2. 最小生成树:Kruskal算法需要使用并查集来检测是否形成环。
  3. 动态连通性问题:动态维护元素之间的连通关系。

性能分析

由于路径压缩和按秩合并的使用,并查集的时间复杂度接近常数级别。具体来说,查找和合并操作的平均时间复杂度是 O(n), 增长极其缓慢,在实际应用中可以视为常数。

例题

例题1: 判断连通性

给定一组节点和边,判断任意两点是否连通。

def are_connected(edges, n, query):
    # 这里假设这个函数已经写好了。就相当于是上面我们写的寻找代表元函数。
    uf = UnionFind(n)
    for u, v in edges:
        uf.union(u, v)
    # 两个代表元相等说明联通返回真
    return uf.find(query[0]) == uf.find(query[1])

# 示例
edges = [(0, 1), (1, 2), (3, 4)]
n = 5
query = (0, 2)
print(are_connected(edges, n, query))  # 输出: True
例题2: 朋友圈问题

有一个包含 (n) 个人的社交网络,每个人都是一个朋友圈。给定一些朋友关系,请计算有多少个独立的朋友圈。

def find_circle_num(M):
    n = len(M)
    uf = UnionFind(n)
    # 同样就是不断地去找,然后计数就行。
    for i in range(n):
        for j in range(i+1, n):
            if M[i][j] == 1:
                uf.union(i, j)
    return len(set(uf.find(i) for i in range(n)))

# 示例
M = [
    [1, 1, 0],
    [1, 1, 0],
    [0, 0, 1]
]
print(find_circle_num(M))  # 输出: 2

下面的这道题,就是用并查集来解决。

3.解决方案

给定一个序列,通过n-1次相邻元素的合并操作,恢复原始序列

关于这道题,我们先看一位up主的解决方案。(@bilbil轩哥码题)


#include <iostream>
using namespace std;

const int N  = 1e7;

int n ,fa[N],so[N],nxt[N];

void init(int n){
    for(int i=1;i<=n;i++){
        fa[i] = i;
        so[i] = i;
    }
}

// 这里实际上就是数组法,每次都要去遍历寻找
int find(int x ){
    // x本身不是自己的父节点,说明x不是一个根结点
    // 则递归调用当前函数,一直找到当前节点的根结点,后面那段代码表示去找当前节点的父节点的父节点,直到找到根结点
    return x ==fa[x] ? x:(fa[x] = find(fa[x]));
}


void merge(int i, int j){
    int x = find(i);
    int y = find(j);
    // 表示两个不在一个集合里面
    if(x!=y)
    {   
        // 这里是做合并操作
        nxt[so[x]] = y;
        fa[y] =x;
        so[x] = so[y]; 
        
    }

}

int main(){
    cin>>n;
    // 初始每个元素的父节点为自己
    init(n);
    // 循环合并每个集合
    for(int i =1;i<n;i++){
        int x,y;
        cin>>x>>y;
        merge(x,y);
    }
    // 这里是每个集合都合并了,然后按照根结点开始遍历这棵树
    // 所以find(1)这里的数字是什么都可以的,然后每次循环更新游标为根结点的儿子就好
    for (int i=find(1);i;i = nxt[i]){
        cout<<i<<" ";
    }

    return 0;
}

这里很显然可以看到他的思路就是我们上面说的数组法,每次寻找父节点都需要去遍历一遍。 那么下面呢,我们按照树去优化一下。

# 初始化
def init(n):
    global fa, so, nxt
    fa = list(range(n + 1))  # 父节点初始化为自己
    so = list(range(n + 1))  # 儿子节点初始化为自己
    nxt = [0] * (n + 1)      # next节点初始化为0

# 查找根节点并进行路径压缩
def find(x):
    if x == fa[x]:
        return x
    fa[x] = find(fa[x])
    return fa[x]

# 合并两个节点
def merge(i, j):
    x = find(i)
    y = find(j)
    if x != y:
        nxt[so[x]] = y
        fa[y] = x
        so[x] = so[y]

# 主函数
def slime_fusion(n, fusion_steps):
    init(n)
    for x, y in fusion_steps:
        merge(x, y)
    
    result = []
    i = find(1)
    while i:
        result.append(i)
        i = nxt[i]
    
    return result

# 输入处理
n = int(input().strip())
fusion_steps = [tuple(map(int, input().strip().split())) for _ in range(n-1)]

# 计算并输出结果
result = slime_fusion(n, fusion_steps)
print(" ".join(map(str, result)))

并查集浅谈,希望对你有所帮助

END

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1813168.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MFC案例:利用SetTimer函数编写一个“计时器”程序

一、希望达成效果 利用基于对话框的MFC项目&#xff0c;做一个一方面能够显示当前时间&#xff1b;另一方面在点击开始按钮时进行读秒计时&#xff0c;计时结果动态显示&#xff0c;当点击结束时读秒结束并保持最后结果。 二、编程步骤及相关代码、注释 1、启动VS…

OA协同办公系统 iWebPDF插件安装

1、下载压缩文件 iweboffice&#xff0c;并进行解压 链接&#xff1a;https://pan.baidu.com/s/1GQd7000PTZ771ifL5KEflg 提取码&#xff1a;hb56 2、安装iWenpdf2018.exe 3、安装金格中间件外部应用 4、测试了谷歌、360安全&#xff0c;发现安装插件后&#xff0c;只有360极…

BP8519C非隔离降压型恒压芯片

BP8519封装和丝印 BP8519封装和丝印 注意&#xff1a; 该芯片为非隔离ACDC电源芯片&#xff0c;非专业人员请勿使用。专业人员在使用时必须注意防护&#xff0c;避免触电。 非隔离ACDC电源芯片&#xff0c;国内有多家半导体厂商生产&#xff0c;在部分追求低价格的低端仪表、灯…

vivado HW_SIO_GTGROUP、HW_SIO_IBERT

HW_SIO.GTGROUP 描述 GT组与硬件设备上的GT IO组相关&#xff0c;具有可用的数量 GT引脚和组由目标Xilinx FPGA确定。在Kintex-7 xc7k325部件上&#xff0c;用于 例如&#xff0c;有四个GT组&#xff0c;每个组包含四个差分GT引脚对。每个GT pin有自己的接收器hw_sio_rx和发射器…

人工智能GPT-4o?

对比分析 在讨论GPT-4o时&#xff0c;我们首先需要了解其前身&#xff0c;即GPT-4&#xff0c;以及其之前的版本。GPT系列从GPT-1到GPT-4经历了多次迭代&#xff0c;每一次都带来了显著的进步。 GPT-4 vs GPT-4o&#xff1a; 1. **参数规模&#xff1a;** GPT-4o在参数规模上…

PyTorch 张量数据类型

【数据类型】Python 与 PyTorch 常见数据类型对应&#xff1a; 用 a.type() 获取数据类型&#xff0c;用 isinstance(a, 目标类型) 进行类型合法化检测 >>> import torch >>> a torch.randn(2,3) >>> a tensor([[-1.7818, -0.2472, -2.0684],[ 0.…

iOS ------ 对象的本质

一&#xff0c;OC对象本质&#xff0c;用clang编译main.m OC对象结构都是通过基础的C/C结构体实现的&#xff0c;我们通过创建OC文件及对象&#xff0c;将OC对象转化为C文件来探寻OC对象的本质。 代码&#xff1a; interface HTPerson : NSObject property(nonatomic,strong)…

什么是SOLIDWORKS科研版

随着科技的不断进步&#xff0c;工程设计和科学研究变得越来越复杂&#xff0c;需要更强大的工具来满足需求。SOLIDWORKS科研版就是在这样的背景下诞生的&#xff0c;它为科研人员和工程师提供了一套全方面、快捷的解决方案&#xff0c;以应对各种科研和工程挑战。 SOLIDWORKS科…

Surface安装Windows和Ubuntu双系统方法(包括Ubuntu适配触控屏的方法)

这是一个目录0.0 前言让我们从一块砖头开始现在你有了能进入windows系统的surface并且想安装Ubuntu现在Ubuntu也有了再见 前言 之前我的Surface装上Ubuntu了好好的&#xff0c;能用&#xff0c;但是Ubuntu原本的内核是不支持很多Surface的功能的&#xff0c;比如触控屏&#xf…

串口调试助手软件(ATK-XCOM) 版本:v2.0

串口设置 软件启动后&#xff0c;会自动搜索可用的串口&#xff0c;可以显示详细的串口信息&#xff0c;由于兼容性原因某些电脑可能不会显示。 超高波特率接收&#xff0c;在硬件设别支持的情况下&#xff0c;可自定义波特率&#xff0c;点“自定义”即可输入您想要的波特率&…

macOS 15 beta (24A5264n) Boot ISO 原版可引导镜像下载

macOS 15 beta (24A5264n) Boot ISO 原版可引导镜像下载 iPhone 镜像、Safari 浏览器重大更新、备受瞩目的游戏和 Apple Intelligence 等众多全新功能令 Mac 使用体验再升级 请访问原文链接&#xff1a;https://sysin.org/blog/macOS-Sequoia-boot-iso/&#xff0c;查看最新版…

PR基础常识

Pr主要就是用来做视频后期剪辑的。是一款非线性视频剪辑软件。它可以将原有视频作为素材&#xff0c;导入到软件的时间线轨道面板中&#xff0c;对视频进行重新剪辑编排&#xff0c;并可以添加文字、图片、音频等素材文件&#xff0c;也能预设各种效果&#xff0c;让剪辑的视频…

外卖抢单神器

在现代快节奏的生活中&#xff0c;外卖服务已成为许多人日常生活的一部分&#xff0c;给外卖行业带来前所未有的机遇和挑战。随着市场竞争的加剧&#xff0c;许多外卖员开始寻求方法以提升接单效率。但在此过程中&#xff0c;道德和合规性是业务持续性的关键。 正直的经营不仅…

Apple Intelligence 带来的十大影响:人工智能的iPhone时刻到来

引言 在最近的WWDC大会上&#xff0c;Apple发布了全新的Apple Intelligence&#xff0c;引起了全球的广泛关注。这次发布被誉为“人工智能的iPhone时刻”&#xff0c;标志着我们每个人都将拥有第一个AI助理&#xff0c;并将引领AI Agent进入红海时代。本文将详细分析Apple Int…

Jetpack Compose Navigation 遇上类型安全

Jetpack Compose Navigation 遇上类型安全 引言 随着 Navigation 2.8.0-alpha08 版本的发布&#xff0c;Navigation 组件引入了基于 Kotlin Serialization 的完整类型安全系统&#xff0c;用于在使用 Kotlin DSL 时定义导航图。这一新特性旨在与 Navigation Compose 等集成最…

1.Anaconda-创建虚拟环境的手把手教程

文章目录 介绍&#xff08;必看&#xff09;正文版本信息模块安装流程1.创建虚拟环境2.激活环境3.退出虚拟环境4.安装python(激活虚拟环境)5.安装tensorflow(激活虚拟环境)6.安装matplotlib7.protobuf版本太高会有问题(激活虚拟环境) 常用的指令&#xff08;一定会用到&#xf…

【CSAPP导读】导论

目录 &#x1f308; 前言&#x1f308; &#x1f4c1; 书籍介绍 &#x1f4c1; 阅读路线 &#x1f4c1; 总结 &#x1f308; 前言&#x1f308; 《深入理解计算机系统》书籍是由布赖恩特(Bryant,R.E.)著的一本经典计算机科学教材&#xff0c;常被简称为"CSAPP"&a…

找不到msvcr110.dll怎么办,msvcr110.dll丢失的7个不同能修复的解决方法

msvcr110.dll是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;由Microsoft Corporation开发&#xff0c;是Visual C Redistributable for Visual Studio 2012的一部分。这个文件包含了运行时库函数&#xff0c;这些函数对于基于Visual C 2012编译的应用程序来说是…

linux笔记8--安装软件

文章目录 1. PMS和软件安装的介绍2. 安装、更新、卸载安装更新ubuntu20.04更新镜像源&#xff1a; 卸载 3. 其他发行版4. 安装第三方软件5. 推荐 1. PMS和软件安装的介绍 PMS(package management system的简称)&#xff1a;包管理系统 作用&#xff1a;方便用户进行软件安装(也…

✔️Vue基础++

✔️Vue基础 组件通信 什么是组件通信&#xff1f; 组件通信就是指 组件与组件 之间的 数据传递 组件的数据是独立的&#xff0c;无法直接访问其他组件的数据想使用其他组件的数据&#xff0c;就需要组件通信 组件之间如何通信&#xff1f; 组件关系 父子关系非父子关系 …