算法-并查集

目录

什么是并查集

并查集基础

(1)原理

(2)初始化

(3)查询

(4)合并

(5)判断是否同一集合

并查集优化

路径压缩

启发式合并

并查集模板

模板

例题

带权并查集

例题

分析

Code


什么是并查集

        并查集是一种树形的数据结构,我们可以使用它来进行集合上的合并与查询等问题。具体来说,它支持两种操作:

  1. 合并:将两个集合合并成一个集合。
  2. 查询:确定某个元素处于哪个集合。

        如图,{3,1,2,4,0,10}{3,1,2,4,0,10} 表示一个集合,{5,7,8,11}{5,7,8,11} 表示另一组集合。

        可以看出并查集是多叉树结构,我们用根节点来表示这个根节点所在的集合(即根节点作为集合的"代表元素")。


并查集基础

(1)原理

        从代码层面,我们如何将两个元素添加到同一个集合中呢?        

        我们将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢。

        只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。代码如下:

// 将v,u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    p[v] = u;
}

       这样我们就可以知道 A 连通 B,因为 A 是索引下标,根据 father[A]的数值就知道 A 连通 B。那怎么知道 B 连通 A呢?

        如果我们的目的是判断这三个元素是否在同一个集合里,知道 A 连通 B 就已经足够了。这里要讲到寻根思路,只要 A ,B,C 在同一个根下就是同一个集合。

        给出A元素,就可以通过 father[A] = B,father[B] = C,找到根为 C。给出B元素,就可以通过 father[B] = C,找到根也为为 C,说明 A 和 B 是在同一个集合里。 大家会想第一段代码里find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下:

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u; // 如果根就是自己,直接返回
    else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}

(2)初始化

        在我们初始创建数据的时候,由于没有任何操作,所以每个元素都是一个独立的集合,显然,每个元素都是本身集合的根节点。

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        p[i] = i;
    }
}

(3)查询

        假设我们现在要查询元素 0 的父节点,该怎么做呢?

        很简单,由于根节点的父节点就是本身(不知道的可以回顾一下初始化过程)。所以我们直接检查 0 的父节点是否为 0 即可。

  1. 如果 0 父节点为 0 ,说明 0 是所属集合的根节点,返回 0 即可。(因为我们用根节点代表集合)
  2. 如果 0 父节点不为 0 ,那么我们只需要递归检查它的父节点是否为 0 即可。

我们发现 0 的父节点是 2 ,那么我们继续检查 2 是否为根节点 (p[2] == 2) ,不是,则继续检查 3 ,此时 3 为根节点,于是返回 3 。

        查询的复杂度为被查询元素在树上的深度。

int find(int u){
    return p[x] == x ? x : find(p[x]);
}

(4)合并

        如图,如何合并 6 所属集合和 3 所属集合?由于我们知道根节点代表整个集合,合并 6 和 3 即意味着它们合并后根节点相同,我们可以任意取一个子集的根节点作为合并后的根节点,比如取 3 后:

        我们选择了把 2 作为合并后集合的根节点(代表元素)。

void merge (int u, int v) {
    u = find(u);
    v = find(v); // x 和 y 为根节点
    p[u] = v; // 直接把其中一个集合合并到另外一个集合
}

(5)判断是否同一集合

        最后我们如何判断两个元素是否在同一个集合里,如果通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合。

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

并查集优化

路径压缩

        我们发现,由于每次查询某个元素需要查询 𝑟 次(𝑟 为当前元素在树上的深度),当树的深度很大,且我们要查询的元素在很深的地方,那么查询所需要耗费的时间就很大,有没有办法优化呢?

        答案是肯定的,我们发现,整个集合只有代表元素是'有用'的,其他元素仅能代表它在这个集合中,与它所处的位置没有关系。 于是,我们在每次查询后,就把当前元素的父节点设置为集合的根节点,根节点就是 𝑓𝑖𝑛𝑑 的返回值,所以:

int find (int x) // find 函数返回x所属集合代表元素
{
    return p[x] == x ? x : p[x] = find(p[x]); // 把x的父节点设置为根节点
}

启发式合并

        上述提到,树的深度会影响查询的速度,那么我们可以在合并的时候,把集合元素较少的合并到集合元素较大的即可。还可以按照集合树的深度与集合的元素数量评估来得到更好的合并方法。

void merge(int u, int v) // 按秩合并需要用到集合内的数量
{
    u = find(u); // 找到节点 u 的根
    v = find(v); // 找到节点 v 的根
    if (size[u] > size[v]) {
        swap(u, v); // 如果节点 u 所在集合的大小大于节点 v 所在集合的大小,则交换它们
    }
    size[v] += size[u]; // 更新节点 v 所在集合的大小
    p[u] = v; // 将节点 u 所在集合的根连接到节点 v 所在集合的根上
}

并查集模板

模板

const int N = 200010;

int p[N]; // p[i] 表示节点 i 的父节点

// 初始化并查集
void init(int n) {
    for (int i = 0; i < n; i++) {
        p[i] = i; // 初始化每个节点的父节点为自身
    }
}

// 查找节点 u 的根,并进行路径压缩
int find(int u) {
    return p[u] == u ? u : p[u] = find(p[u]); // 如果节点 u 的父节点不是自身,则递归查找其父节点,并进行路径压缩
}

// 将节点 u 和节点 v 所在的集合合并
void merge(int u, int v) {
    u = find(u); // 寻找节点 u 的根
    v = find(v); // 寻找节点 v 的根
    if (u == v) return; // 如果节点 u 和节点 v 已经在同一个集合中,则不需要连接,直接返回
    p[v] = u; // 将节点 v 的根连接到节点 u 的根上
}

// 判断节点 u 和节点 v 是否属于同一个集合
bool isSame(int u, int v) {
    u = find(u); // 寻找节点 u 的根
    v = find(v); // 寻找节点 v 的根
    return u == v; // 如果节点 u 和节点 v 的根相同,则它们属于同一个集合,返回 true,否则返回 false
}

例题1

684. 冗余连接 - 力扣(LeetCode)

Code

int n = 1005;

// 并查集初始化
void init(int* father) {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}

// 并查集里寻根的过程
int find(int u, int* father) { 
    return u == father[u] ? u : (father[u] = find(father[u], father)); 
}

// 判断 u 和 v 是否找到同一个根
bool isSame(int u, int v, int* father) {
    u = find(u, father);
    v = find(v, father);
    return u == v;
}

// 将 v->u 这条边加入并查集
void join(int u, int v, int* father) {
    int rootU = find(u, father); // 寻找u的根
    int rootV = find(v, father); // 寻找v的根
    if (rootU == rootV) {
        return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    }
    father[rootV] = rootU;
}

int* findRedundantConnection(int** edges, int edgesSize, int* edgesColSize, int* returnSize) {
    int father[n];
    init(father);
    for (int i = 0; i < edgesSize; i++) {
        if (isSame(edges[i][0], edges[i][1], father)) {
            *returnSize = 2;
            return edges[i];
        } else {
            join(edges[i][0], edges[i][1], father);
        }
    }
    *returnSize = 0;
    return NULL;
}

例题2 

685. 冗余连接 II - 力扣(LeetCode) 

n个节点有n-1个分支,多出1个分支存在两种情况:

    1.所有节点出度为1,即肯定形成了环,取形成环的节点(u和v同父)

    2.存在节点出度为2,表现在v节点入树时已经存在父节点,说明v有两个父节点,当前的边u,v冲突 

Code 

int* ancestor;

int find(int index) {
    return index == ancestor[index] ? index : (ancestor[index] = find(ancestor[index]));
}

void merge(int u, int v) {
    ancestor[find(u)] = find(v);
}

int* findRedundantDirectedConnection(int** edges, int edgesSize, int* edgesColSize, int* returnSize) {
    int n = edgesSize;
    ancestor = malloc(sizeof(int) * (n + 1));//并查集
    for (int i = 1; i <= n; ++i) {
        ancestor[i] = i;
    }
    int parent[n + 1];//记录树的父节点
    for (int i = 1; i <= n; ++i) {
        parent[i] = i;
    }
    int conflict = -1;
    int cycle = -1;
    for (int i = 0; i < n; ++i) {
        int node1 = edges[i][0], node2 = edges[i][1];
        if (parent[node2] != node2) {//计入v时发现v有两个父节点,标记冲突节点
            conflict = i;
        } else {
            parent[node2] = node1;
            if (find(node1) == find(node2)) {//u和v同父,标记环节点
                cycle = i;
            } else {
                merge(node1, node2);
            }
        }
    }
    int* redundant = malloc(sizeof(int) * 2);
    *returnSize = 2;
    if (conflict < 0) {//不存在冲突
        redundant[0] = edges[cycle][0], redundant[1] = edges[cycle][1];
        return redundant;
    } else {//存在冲突
        int* conflictEdge = edges[conflict];
        if (cycle >= 0) {//同时存在环, redundant[0]为冲突[u,v]中v的父节点
            redundant[0] = parent[conflictEdge[1]], redundant[1] = conflictEdge[1];
            return redundant;
        } else {//不存在环
            redundant[0] = conflictEdge[0], redundant[1] = conflictEdge[1];
            return redundant;
        }
    }
    return redundant;
}

        为什么冲突和有环是 redundant[0] = parent[conflictEdge[1]], redundant[1] = conflictEdge[1];呢?举例子[[2,1],[3,1],[4,2],[1,4]]如图所示:


带权并查集

        当然,维护了数量在某些情况也是不够用的,我们还需要知道集合内各个元素的关系。我们可以使用带权并查集,使用边权来维护当前元素与父节点的某种关系。即,带权并查集可以维护元素之间的制约关系。我们以一道经典例题为例。

例题

动物王国中有三类动物 A, B, C,这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N 编号。

每个动物都是 A, B, C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 X 或 Y 比 N 大,就是假话;
  3. 当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

分析

        给出两个动物,它们有吃、被吃以及同类三种制约关系,而带权并查集可以很好地维护元素间的制约关系。

        设 d[x] 表示元素 x 与其父节点的边的边权,规定:

  • d[x] % 3 = 0 表示 x 与父节点 p[x] 是同类。
  • d[x] % 3 = 1 表示 x 可以吃父节点 p[x]。
  • d[x] % 3 = 2 表示 x 可以被父节点 p[x] 吃。

        那么我们判定假话,只需要不满足 d[x] 即可。

        简单来说:

  • 判断 x 与 y 为同类,但已经制约了 x 和 y 为异类(吃或被吃)。
  • 判断 x 吃 y ,但已经制约了 x 和 y 是同类或者 x 被 y 吃。
  • 判断 x 被 y 吃,但已经制约了 x 和 y 是同类或者 x 被 y 吃。(题目不会给定)

        首先我们肯定要是有路径压缩来优化查询的,在路径压缩后, x 对应的父节点变为集合根节点,因此 d[x] 也需要做变换。

int find (int x)
{
    if (x != p[x])
    {
        int u = find(p[x]);
        /*
         * 注意此时x还没有路径优化,父节点仍然保持原来的父节点
         * 此时 x 以上的节点经过路径优化,d[p[x]] 也修改为正确值(x父节点与根节点的关系)
         * 那么我们只需要根据x与父节点的关系、x父节点与根节点的关系即可传递得到x与根节点的关系,再路径优化即可。
        */
        d[x] += d[p[x]];
        p[x] = u;
    }
    return p[x];
}

        那么现在的问题就是,如何知道一个集合里两个元素的制约关系?

        由于我们求得 ( d[x] ) 都是 ( x ) 与根节点的关系,那么 ( (d[x] - d[y]) % 3 ) 即为 ( x ) 与 ( y ) 的制约关系。

        如何合并两个关系呢?

        假设判定 ( x ) 和 ( y ) 的关系的边权表示为 ( op ),由于在 find 中我们可以求得 ( x ) 、( y ) 分别与其根节点的关系,且现在 ( x ) 与 ( y ) 的制约关系也知道了,那么根据传递性我们也可以求出两个集合根节点之间的制约关系,合并两个集合时维护好两个根节点的制约关系即可。

        假设 ( x ) 的根节点为 ( p_x ), ( y ) 的根节点为 ( p_y )。现在要把 ( p_x ) 合并到 ( p_y )。

1.判定 ( x ) 与 ( y ) 同类

        在合并后的集合里, ( x ) 与 ( y ) 的关系应该为 ( (d[x] - d[y]) % 3 = 0 )。由于此时的 ( d[x] ) 是合并后的,所以合并前应该为 ( d[x] + d[p_x] )。即 ( d[x] + d[p_x] - d[y] = 0 ),那么 ( d[p_x] = d[y] - d[x] )。

2.判定 ( x ) 与 ( y ) 不同类

        由于题目给定此时判定为 ( x ) 吃 ( y ),所以我们只需要考虑这一种。

        在合并后的集合里, ( x ) 与 ( y ) 的关系应该是: ( d[x] - d[y] = 1 ),即 ( x ) 可以吃根节点(路径压缩后的父节点),且根节点与 ( y ) 同类,依次推类。

        同样此时的 ( d[x] ) 是合并后的,合并前应该是 ( d[x] + d[p_x] ),所以 ( d[x] + d[p_x] - d[y] = 1 ),即 ( d[p_x] = 1 + d[y] - d[x] )。


Code

#include <stdio.h>

const int N = 50010;

int n, m;
int p[N], d[N];

int find(int x) {
    if (p[x] != x) {
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main() {
    scanf("%d%d", &n, &m);
    
    for (int i = 1; i <= n; i++) {
        p[i] = i;
        d[i] = 0;  // 将距离模3数组初始化为0
    }
    
    int res = 0;
    while (m--) {
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);
        
        if (x > n || y > n) {
            res++; // 谎言类型1:动物编号超出限制
        } else {
            int px = find(x), py = find(y);
            if (t == 1) {
                if (px == py && (d[x] - d[y]) % 3 != 0) res++; // 谎言类型2:声称相同种类但约束条件不成立
                else if (px != py) {
                    p[px] = py;
                    d[px] = (d[y] - d[x] + 3) % 3;  // 确保结果非负
                }
            }
            else {
                if (px == py && (d[x] - d[y] - 1) % 3 != 0) res++; // 谎言类型3:声称x吃y但约束条件不成立
                else if (px != py) {
                    p[px] = py;
                    d[px] = (d[y] + 1 - d[x] + 3) % 3; // 确保结果非负
                }
            }
        }
    }
    
    printf("%d\n", res);
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

线下订单平台操作步揍

收款管理 1微信收款查询 1. 获取微信数据 获取微信数据。通过时间范围 查找微信数据调用第三方接口如下&#xff1a; Map map HttpPost.doPost("https://qyapi.weixin.qq.com/cgi-bin/externalpay/get_bill_list?access_token"ApiUtils.getWxtoken(),args); 其中…

如何缩小图片尺寸不改变清晰度?几个方法教你解决

在平时对图片进行处理的时候&#xff0c;最害怕的就是修改过的图片质量下降&#xff0c;导致清晰度不够&#xff0c;尤其是缩小图片尺寸的时候&#xff0c;所以今天小编就来告诉大家几个关于修改图片尺寸又不改变清晰度的方法。 修改图片大小是非常普遍的图片编辑需求&#xf…

【SpringMVC 】什么是SpringMVC(三)?基于springmvc的文件上传、基于springmvc的拦截器、基于springmvc的邮件发送

文章目录 SpringMVC第五章1、SpringMVC文件上传1、基本步骤1-2345-82、邮件发送1、基本步骤1-234-5567-8 简单邮件带附件的邮件第六章1、拦截器的使用使用步骤232、调度的使用基本步骤1-56-8调度规则3、shiro安全框架核心概念基本语法1、基于ini文件的认证**测视类**2、基于rea…

计算机组成原理网课笔记

无符号整数的表示与运算 带符号整数的表示与运算 原反补码的特性对比 移码

基于 docker-compose 部署 LNMP 架构

目录 前言 1、任务要求 2、Nginx 2.1 建立工作目录并上传相关安装包 2.2 编写 Nginx Dockerfile 脚本 2.3 准备 nginx.conf 配置文件 3、Mysql 3.1 建立工作目录并上传相关安装包 3.2 编写 Mysql Dockerfile 脚本 3.3 编写 my.cnf 配置文件 4、PHP 4.1 建立工作目录…

Spring MVC(一)

1 Spring MVC概述 我们在之前学习Servlet的时候&#xff0c;认识了在WEB开发中MVC设计模式&#xff0c;其最为经典的设计就是&#xff0c;通过控制器&#xff08;Controller&#xff09;分离模型&#xff08;Model&#xff09;和视图&#xff08;View&#xff09;。在具体的WEB…

提高谷歌抓取成功率:代理IP的7个使用误区

在当今数字化时代&#xff0c;数据采集和网络爬取已成为许多企业和个人必不可少的业务活动。对于爬取搜索引擎数据&#xff0c;特别是Google&#xff0c;使用代理IP是常见的手段。然而&#xff0c;使用代理抓取Google并不是一件轻松的事情&#xff0c;有许多常见的误区可能会导…

在IDEA中通过模块创建新项目的时候,出现无法连接的错误

1.找到IDEA中的设置 2.在设置搜索HTTP,选择自动检测代理设置 选择URL: 输入https://start.spring.io 3.点击应用&#xff0c;即可完成

面试算法-链表-反转链表(golang、c++)

目录 1、题目 2、解题思路 2.1 遍历、迭代 2.2 递归 3、源代码 3.1 c 3.2 golang 4、复杂度分析 4.1 遍历、迭代法 4.2 迭代法 1、题目 链表是一种常用的数据结构&#xff0c;链表的特点是插入、删除节点的效率非常高&#xff0c;因为他不需要移动其他任何元素&…

nginx--防盗链

盗链 通过在自己网站里面引用别人的资源链接,盗用人家的劳动和资源 referer referer是记录打开一个页面之前记录是从哪个页面跳转过来的标记信息 正常的referer信息 none&#xff1a;请求报文首部没有referer首部&#xff0c;比如用户直接在浏览器输入域名访问web网站&…

使用 Cython 加密 Python 代码防止反编译

文章目录 前言使用 Cython 加密 Python 代码环境Python 源代码编写 Cython 编译配置文件 编译查看输出文件使用 问题error: Microsoft Visual C 14.0 or greater is requiredpyconfig.h(59): fatal error C1083: 无法打开包括文件: “io.h”: No such file or directorydynamic…

【已解决】‘pip‘ 不是内部或外部命令问题

&#x1f60e; 作者介绍&#xff1a;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff0c;视频号&#xff1a;AI-行者Sun &#x1f388; 本文专栏&#xff1a;本文收录于《AI实战中的各种bug…

大模型微调之 在亚马逊AWS上实战LlaMA案例(三)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;三&#xff09; 使用 QLoRA 增强语言模型&#xff1a;Amazon SageMaker 上 LLaMA 2 的高效微调 语言模型在自然语言处理任务中发挥着关键作用&#xff0c;但训练和微调大型模型可能会占用大量内存且耗时。在本文中&…

Springboot整合飞书向群组/指定个人发送消息/飞书登录

Springboot整合飞书向群组发送消息 飞书开放平台创建企业自建应用 添加应用能力-机器人 创建完成后&#xff0c;进入应用详情页&#xff0c;可以在首页看到 App Id 和 App Secret 在飞书pc端创建一群机器人 此处可以拿到该机器人的webhook地址,通过https的方式,也可以调用发送…

为什么说RK3562可以碾压PX30?

在如今的科技市场中&#xff0c;处理器的性能直接决定了设备的运行速度和用户体验。今天&#xff0c;我们将对比瑞芯微旗下的两款处理器&#xff1a;PX30与RK3562。RK3562比PX30的性价比究竟高在哪里&#xff1f; PX30 瑞芯微PX30是一款高性能的四核应用处理器&#xff0c;专…

Android单行字符串末尾省略号加icon,图标可点击

如图 设置仅显示单行字符串&#xff0c;末尾用省略号&#xff0c;加跟一个icon&#xff0c;icon可点击 tvName.text "test"val drawable ResourcesCompat.getDrawable(resources, R.mipmap.icon_edit, null)tvName.setCompoundDrawablesWithIntrinsicBounds(null,…

故障——蓝桥杯十三届2022国赛大学B组真题

问题分析 这道题纯数学&#xff0c;考察贝叶斯公式 AC_Code #include <bits/stdc.h> using namespace std; typedef pair<int,double> PI; bool cmp(PI a,PI b){if(a.second!b.second)return a.second>b.second;return a.first<b.first; } int main() {i…

在Leaflet中点对象使用SVG和Canvas两种模式的对比

目录 前言 一、关于SVG和Canvas 1、SVG知识 2、Canvas知识 3、优缺点 二、SVG和Canvas在Leaflet的使用 1、相关类图 2、Leaflet的默认展示方式 三、SVG和Canvas实例及性能对比 1、SVG模式及性能对比 2、Canvas优化 总结 前言 众所周知&#xff0c;在Leaflet当中&#…

vue3配置element-plus时间选择器中文显示

修改main.js import ElementPlus from element-plus import element-plus/dist/index.css // 引入中文包 import zhCn from "element-plus/es/locale/lang/zh-cn"; const app createApp(App) app.use(ElementPlus,{ locale: zhCn, }) //挂载 app.mount(#app)

白盒测试:覆盖测试及测试用例设计

白盒测试&#xff1a;覆盖测试及测试用例设计 一、实验目的 1、掌握白盒测试的概念。 2、掌握逻辑覆盖法。 二、实验任务 某工资计算程序功能如下&#xff1a;若雇员月工作小时超过40小时&#xff0c;则超过部分按原小时工资的1.5倍的加班工资来计算。若雇员月工作小时超过…