数据结构-并查集专题(1)

news2024/11/13 10:48:05

一、前言

因为要开始准备年底的校赛和明年年初的ACM、蓝桥杯、天梯赛,于是开始按专题梳理一下对应的知识点,先从简单入门又值得记录的内容开始,并查集首当其冲。

二、我的模板

虽然说是借用了jiangly鸽鸽的板子,但是自己也小做修改了部分(命名啥的,大体内容并没有修改,jiangly鸽鸽yyds)

#include <bits/stdc++.h>

struct DisjointSet {
    int _n;
    std::vector<int> _fa,_size;
    DisjointSet(){}
    DisjointSet(int n){
        init(n);
    }
    void init(int n) {
        _fa.resize(n);
        std::iota(_fa.begin(),_fa.end(),0);
        _size.assign(n,1);
    }
    int find(int x) {
        if(x!=_fa[x]) {
            _fa[x] = find(_fa[x]);
        }
        return _fa[x];
    }
    bool same(int x,int y) {
        return find(x)==find(y);
    }
    bool merge(int x,int y) {
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy) {
            _size[fx]+=_size[fy];
            _fa[fy] = fx;
            return true;
        }
        return false;
    }
};

三、并查集介绍

并查集(disjoint set),英文直译过来是不相交的集合。我们中文取名成并查集,是因为这类集合主要具有两个操作:并和查,并即合并两个集合,查即查询集合的某些信息。

3.1 并

如果有学习过树的知识,那么理解并查集就比较轻松了。“并”操作类似于将森林(两棵树)转化为一棵树,我们只需要让其中一个并查集的根节点的父亲结点指向另外一个并查集的根节点即可。当然选择的时候,我们倾向于让深度小的树的根节点指向深度大的树的根节点。不过后续用上路径压缩后,谁指向谁基本没有什么太大的影响。实际写代码中,我们主要的操作就是对一个并查集的根节点进行修改

3.2 查

1.查询某个节点的根节点 2.查询两个节点是否处于同一个并查集中 3.查询一共有几个并查集 4.查询节点数量最多的并查集和它的节点数量 5.查询节点位于自身并查集的深度

3.3 初始化

在没有进行任何合并前,一个并查集的根节点应该为它自身(切记写代码的时候不要忘记并查集的初始化,当然如果用我的模板的话就不会忘记初始化,构造函数已经写好初始化了)

3.4 路径压缩

正常来说,并查集合并的时间复杂度为O(1),而查询的最坏时间复杂度为O(n),常见的最坏情况就是只有左子树或者只有右子树的一棵树的查询。而如果我们在查询时顺带将查询路径上的结点的父节点属性顺带修改为真正的根节点,那么综合下来,时间复杂度将会被均摊成O(logn),这是一个非常优秀的优化。

四、专题训练

以我学校OJ题目展开训练

4.1 题目总览

4.2 1520: 数据结构-并查集-家庭问题

思路

这道题是很典型的用并查集做的题目,先对并查集进行初始化,我们直接把有关系的两人进行合并,如果已经处于同一个并查集中则不做操作。后续题目需要我们查询的是并查集的数量以及并查集中最大的节点数量

参考代码

前面都是并查集模板,注意构造并查集的时候至少把节点数+1(因为用于实现的vetcor下标从0开始,大部分题目的下标是从1开始的),最后查询的时候利用set进行排序输出即可

#include <bits/stdc++.h>
using i64 = long long;

struct DisjointSet {
    int _n;
    std::vector<int> _fa,_size;
    DisjointSet(){}
    DisjointSet(int n){
        init(n);
    }
    void init(int n) {
        _fa.resize(n);
        std::iota(_fa.begin(),_fa.end(),0);
        _size.assign(n,1);
    }
    int find(int x) {
        if(x!=_fa[x]) _fa[x] = find(_fa[x]);
        return _fa[x];
    }
    bool same(int x,int y) {
        return find(x)==find(y);
    }
    bool merge(int x,int y) {
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy) {
            _size[fy]+=_size[fx];
            _fa[fx] = fy;
            return true;
        }
        return false;
    }
};
using pii = std::pair<int,int>;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n,k;
    std::cin >> n >> k;
    DisjointSet disjointSet = DisjointSet(n+1);
    for(int i = 0;i<k;i++) {
        int mem1,mem2;std::cin >> mem1 >> mem2;
        disjointSet.merge(mem1,mem2);
    }
    std::set<pii> st;
    for(int i = 1;i<=n;i++) {
        int t = disjointSet.find(i);
        st.insert(std::make_pair(disjointSet._size[t],t));
    }
    std::cout << st.size() << ' ' << (*st.rbegin()).first << '\n';

    return 0;
}

4.3 1565: 并查集-团伙

思路

这个题由于enemy的存在,需要多维护一个ene[]数组,ene[]数组初始化为0,如果遇到p等于0的时候,分别判断x和y的ene是否为0,如果为0,则代表他们当前没有enemy,则把ene值设置成对方,即x和y分别属于两个并查集中,如果当前存在ene,那么就把自己合并到他们enemy的并查集中,因为敌人的敌人是朋友。至于p等于1的情况,当做正常并查集的合并来做。最后输出并查集的数量(计算父节点等于自身的节点数量)即可

参考代码

#include <bits/stdc++.h>
using i64 = long long;

struct DisjointSet {
    int _n;
    std::vector<int> _fa,_size;
    DisjointSet(){}
    DisjointSet(int n){
        init(n);
    }
    void init(int n) {
        _fa.resize(n);
        std::iota(_fa.begin(),_fa.end(),0);
        _size.assign(n,1);
    }
    int find(int x) {
        if(x!=_fa[x]) {
            _fa[x] = find(_fa[x]);
        }
        return _fa[x];
    }
    bool same(int x,int y) {
        return find(x)==find(y);
    }
    bool merge(int x,int y) {
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy) {
            _size[fy]+=_size[fx];
            _fa[fx] = fy;
            return true;
        }
        return false;
    }
};

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n,m;
    std::cin >> n >> m;
    DisjointSet disjointSet = DisjointSet(n+1);
    std::vector<int> ene(n+1,0);

    for(int i = 0;i<m;i++) {
        int t,x,y;std::cin >> t >> x >> y;
        if(t) {//enemy
            if(ene[x]) {
                disjointSet.merge(y,ene[x]);
            }else {
                ene[x] = y;
            }
            if(ene[y]) {
                disjointSet.merge(x,ene[y]);
            }else {
                ene[y] = x;
            }
        }else {//friend
            disjointSet.merge(x,y);
        }
    }

    int ans = 0;
    for(int i = 1;i<=n;i++) {
        if(disjointSet.find(i)==i) ++ans;
    }
    std::cout << ans << '\n';

    return 0;
}

4.4 1566: 并查集-打击犯罪

思路

题目有点反直觉,我一开始想着从1到k依次进行判断,断开某个关系后可以使得他们中最大的一个危险程度小于等于n/2,然后后来发现写起来很困难。正难则反,因此我们考虑从n开始循环,跑到1结束,每次循环让这个循环变量i这个节点和它有关系并且大于k的j节点进行合并,然后看看是否有危险程度大于n/2的并查集即可。

参考代码

#include <bits/stdc++.h>
using i64 = long long;

struct DisjointSet {
    int _n;
    std::vector<int> _fa,_size;
    DisjointSet(){}
    DisjointSet(int n){
        init(n);
    }
    void init(int n) {
        _fa.resize(n);
        std::iota(_fa.begin(),_fa.end(),0);
        _size.assign(n,1);
    }
    int find(int x) {
        if(x!=_fa[x]) {
            _fa[x] = find(_fa[x]);
        }
        return _fa[x];
    }
    bool same(int x,int y) {
        return find(x)==find(y);
    }
    bool merge(int x,int y) {
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy) {
            _size[fx]+=_size[fy];
            _fa[fy] = fx;
            return true;
        }
        return false;
    }
};

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n;std::cin >> n;
    std::vector<std::vector<int>> edges(n+1,std::vector<int>());
    DisjointSet disjointSet = DisjointSet(n+1);

    for(int i = 0;i<n;i++) {
        int siz;std::cin >> siz;
        for(int j = 0;j<siz;j++) {
            int t;std::cin >> t;
            edges[i+1].emplace_back(t);
        }
    }
    
    for(int k = n;k>=1;k--) {
        for(auto &edge:edges[k]) {
            if(edge>k) {
                disjointSet.merge(k,edge);
            }
        }
        if(disjointSet._size[disjointSet.find(k)]>n/2) {
            std::cout << k << '\n';
            return 0;
        }
    }

    return 0;
}

4.5 1567: 并查集-搭配购买

思路

并查集+01背包,中间对数据的存储搞得我很头疼,用了一堆vector,先把c[]和d[]数组存起来,做了并查集的合并后,再算一个并查集c[]的总和sumc[]以及d[]的总和sumd[],然后把计算好的总和放进real_c[]和real_d[]数组中,再使用一个dp[]数组做一遍01背包,最后的dp[w]输出即可

参考代码

#include <bits/stdc++.h>
using i64 = long long;

struct DisjointSet {
    int _n;
    std::vector<int> _fa,_size;
    DisjointSet(){}
    DisjointSet(int n){
        init(n);
    }
    void init(int n) {
        _fa.resize(n);
        std::iota(_fa.begin(),_fa.end(),0);
        _size.assign(n,1);
    }
    int find(int x) {
        if(x!=_fa[x]) {
            _fa[x] = find(_fa[x]);
        }
        return _fa[x];
    }
    bool same(int x,int y) {
        return find(x)==find(y);
    }
    bool merge(int x,int y) {
        int fx = find(x);
        int fy = find(y);
        if(fx!=fy) {
            _size[fx]+=_size[fy];
            _fa[fy] = fx;
            return true;
        }
        return false;
    }
};
using pii = std::pair<int,int>;

int main() {
    std::cin.tie(nullptr)->sync_with_stdio(false);

    int n,m,w;
    std::cin >> n >> m >> w;
    std::vector<int> c,d;

    for(int i = 0;i<n;i++) {
        int tmpc,tmpd;
        std::cin >> tmpc >> tmpd;
        c.emplace_back(tmpc);
        d.emplace_back(tmpd);
    }

    DisjointSet disjointSet = DisjointSet(n+1);
    for(int i = 0;i<m;i++) {
        int x,y;std::cin >> x >> y;
        disjointSet.merge(x,y);
    }

    std::vector<int> sumc(n+1,0),sumd(n+1,0);
    for(int i = 1;i<=n;i++) {
        int t = disjointSet.find(i);
        sumc[t] += c[i-1];
        sumd[t] += d[i-1];
    }

    std::vector<int> real_c,real_d;
    for(int i = 1;i<=n;i++) {
        if(sumc[i]!=0) {
            real_c.emplace_back(sumc[i]);
            real_d.emplace_back(sumd[i]);
        }
    }

    std::vector<int> dp(w+1,0);

    for(int i = 0;i<real_c.size();i++) {
        for(int j = w;j>=real_c[i];j--) {
            dp[j] = std::max(dp[j],dp[j-real_c[i]]+real_d[i]);
        }
    }
    std::cout << dp[w] << '\n';

    return 0;
}

五、后记

剩下的题目及并查集的进一步运用(求最小生成树的Kruskal算法)见后续的(2)

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

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

相关文章

二手交易平台测试用例设计和执行

&#x1f384;欢迎来到边境矢梦的csdn博文&#x1f384; &#x1f384;追求开源思想和学无止境思想一直在提升技术的路上 &#x1f384; &#x1f308;涉及的领域有&#xff1a;Java、Python、微服务架构和分布式架构思想、基本算法编程&#x1f308; &#x1f386;喜欢的朋友可…

计算机图形学论文 | 多边形中的点可见性快速算法

&#x1f98c;&#x1f98c;&#x1f98c;读论文 &#x1f428;&#x1f428;摘要 针对点的可见性计算这一计算几何中的基础问题&#xff0c;提出一种支持任意查询点的可见多边形快速计算的基于多边形Voronoi图的点可见性算法。以与Voronoi骨架路径对应的Voronoi通道概念&…

Redis 高并发分布式锁实战

目录 环境准备 一 . Redis 安装 二&#xff1a;Spring boot 项目准备 三&#xff1a;nginx 安装 四&#xff1a;Jmeter 下载和配置 案例实战 优化一&#xff1a;加 synchronized 锁 优化二&#xff1a;使用 redis 的 setnx 实现分布式锁 优化三&#xff1a;使用 Lua 脚本…

LLM大模型学习精华系列:VLLM性能优化部署实践——全面加速从推理到部署的流程

训练后的模型会用于推理或者部署。推理即使用模型用输入获得输出的过程&#xff0c;部署是将模型发布到恒定运行的环境中推理的过程。一般来说&#xff0c;LLM的推理可以直接使用PyTorch代码、使用[VLLM]等框架&#xff0c;也可以使用[llama.cpp]等c推理框架。 常见推理方法 G…

【大数据学习 | kafka高级部分】kafka的快速读写

1. 追加写 根据以上的部分我们发现存储的方式比较有规划是对于后续查询非常便捷的&#xff0c;但是这样存储是不是会更加消耗存储性能呢&#xff1f; 其实kafka的数据存储是追加形式的&#xff0c;也就是数据在存储到文件中的时候是以追加方式拼接到文件末尾的&#xff0c;这…

SpringCloud篇(微服务)

目录 一、认识微服务 1. 单体架构 2. 分布式架构 3. 微服务 3.1. 特点 3.2. 优点 3.3 缺点 二、微服务设计、拆分原则 1. AKF 拆分原则 2. Y轴&#xff08;功能&#xff09;关注应用中功能划分&#xff0c;基于不同的业务拆分 3. X轴&#xff08;水平扩展&#xff09…

Hive简介 | 体系结构

Hive简介 Hive 是一个框架&#xff0c;可以通过编写sql的方式&#xff0c;自动的编译为MR任务的一个工具。 在这个世界上&#xff0c;会写SQL的人远远大于会写java代码的人&#xff0c;所以假如可以将MR通过sql实现&#xff0c;这个将是一个巨大的市场&#xff0c;FaceBook就这…

高校宿舍信息管理系统小程序

作者主页&#xff1a;编程千纸鹤 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参…

森林防火责任大于天,可视化监控大屏让隐患无处遁形。

在大自然的生态系统中&#xff0c;森林是地球之肺&#xff0c;为我们提供着清新的空气、丰富的资源和优美的环境。然而&#xff0c;森林火灾却如同一场可怕的灾难&#xff0c;随时可能摧毁这片宝贵的绿色财富。森林防火责任大于天&#xff0c;而可视化监控大屏的出现&#xff0…

“穿梭于容器之间:C++ STL迭代器的艺术之旅”

引言&#xff1a; 迭代器&#xff08;Iterator&#xff09;是C STL&#xff08;标准模板库&#xff09;中非常重要的一部分&#xff0c;它提供了一种统一的方式来遍历容器中的元素。无论容器是数组、链表、树还是其他数据结构&#xff0c;迭代器都能够以一致的方式访问这些数据…

el-scrollbar 动态更新内容 鼠标滚轮无效

有以下功能逻辑&#xff0c;实现了一个时间轴组件&#xff0c;点击、-号后像地图那样放大组件以显示不同的UI。 默认显示年月&#xff1a; 当点击一下加号时切换为年&#xff1a; 当点击减号时切换为日&#xff1a; 即加号、减号点击就是在年月日显示进行切换。给Scrollvie…

Linux【基础篇】

-- 原生罪 linux的入门安装学习 什么是操作系统&#xff1f; 用户通过操作系统和计算机硬件联系使用。桥梁~ 什么是Linux&#xff1f; 他是一套开放源代码&#xff08;在互联网上找到Linux系统的源代码&#xff0c;C语言写出的软件&#xff09;&#xff0c;可以自由 传播&…

C++类(5)

1.<<和>>操作符重载 我们该如何重载操作符<<和>>呢&#xff1f; 如果在类里面&#xff0c; void operator<<(ostream& out) {out << _year << "年" << _month << "月" << _day <&l…

【MM-Align】学习基于输运的最优对齐动力学,快速准确地推断缺失模态序列

代码地址 - > github传送 abstract 现有的多模态任务主要针对完整的输入模态设置&#xff0c;即每个模态在训练集和测试集中要么是完整的&#xff0c;要么是完全缺失的。然而&#xff0c;随机缺失的情况仍然没有得到充分的研究。在本文中&#xff0c;我们提出了一种新的方…

高精度算法-保姆级讲解

目录 1.什么是高精度算法 2.高精度加法 3.高精度减法 4.高精度乘法 5.高精度除法 &#xff08;高精度除以低精度&#xff09; 6.高精度阶乘&#xff08;n个低精度数相乘&#xff09; 1.什么是高精度算法 高精度算法&#xff08;High Accuracy Algorithm&#xff09;是…

vue大疆建图航拍功能实现

介绍 无人机在规划一块区域的时候&#xff0c;我们需要手动的给予一些参数来影响无人机飞行&#xff0c;对于一块地表&#xff0c;无人机每隔N秒在空中间隔的拍照地表的一块区域&#xff0c;在整个任务执行结束后&#xff0c;拍到的所有区域照片能够完整的表达出一块地表&…

learnopencv系列三:GrabCut和DeepLabv3分割模型在文档扫描应用中的实现

文章目录 一、使用OpenCV实现自动文档扫描1.1 图片预处理1.2 查找轮廓1.3 检测角点1.4 仿射变换1.5 Streamlit Web App1.5.1 设置扫描函数和图像下载链接函数1.5.2 streamlit app1.5.3 测试结果 二&#xff1a;DeepLabv3文档分割2.1 项目背景2.2 合成数据集2.2.1 图像收集与预处…

SQLite的BLOB数据类型与C++二进制存储学习记录

一、BLOB数据类型简介 Blob&#xff08;Binary Large Object&#xff09;是一种用于存储二进制数据的数据类型&#xff0c;在数据库中常用于存储图片、音频和视频等大型&#xff08;大数据量&#xff09;的二进制数据[1-2]。需要注意的是&#xff0c;SQLite中BLOB类型的单对象最…

C# 自己编写web服务

文件后缀响应 "text/html"; 文件后缀响应 "application/json"; httpListenerContext.Response.ContentType 文件后缀响应; httpListenerContext.Response.AppendHeader("Access-Control-Allow-Origin", "*"); // L…

微服务day04

网关 网关路由 快速入门 创建新模块&#xff1a;hm-gateway继承hmall父项目。 引入依赖&#xff1a;引入网关依赖和nacos负载均衡的依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"…