算法—并查集

并查集

概念

并查集是一种维护集合的数据结构。并------合并(合并俩个集合),查------查找(判断两个元素是否存在一个集合),集------集合。

怎么实现?

  • 数组模拟树:father[i]=x;
    • 上述表示元素 i 的父亲节点为 x ,其中,根节点为元素本身,对同一个集合来说只存在一个根结点,且将其作为所属集合的标识

father1=2; //元素1的父亲节点为2

father1=1; //元素1为该集合的根节点

注意

处理并查集时始终用一个函数 来合并集合,避免直接修改父节点 数组(或者修改完父节点数组后再次重新查找父节点),因为这会破坏路径压缩,导致循环引用或造成错误的集合关系,破坏了并查集结构

操作

初始化

  • 每个元素都是一个单独的集合,所有元素的根节点都等于本身
cpp 复制代码
for(int i=1;i<=n;i++){
	father[i]=i;
}+-

查找

根据某节点找到所在集合的根节点,可用递推或递归

递推:

cpp 复制代码
int find(int i){
	while(i!=father[i]){
		i=father[i];	//得到父亲节点继续寻呼那循环
	}
	return i;		//返回根节点
}

递归:

cpp 复制代码
int find(int i){
	if(i==father[i]){
		return i;
	}else{
		return find(father[i]);	
	}
}

合并

思路:

  1. 判断两个元素是否属于同一个集合,只有当两个元素属于不同集合时才合并
  2. 合并是把其中一个集合的根结点的父亲指向另一个集合的根结点。
cpp 复制代码
void union(int i,int j){
	int a=find(i);
	int b=find(j);
	if(a!=b){
		father[a]=b;
	}
}

路径压缩

在题目给出的元素数量很多并且形成一条链,那么这个查找函数的效率就会非常低。因为每次都要回溯相当于遍历了一遍,计算量极大

对于根节点为1树:

正常查找:

father1=1;

father2=1;

father3=2;

father4=3;

如果只是为了查找根结点,那么完全可以想办法把操作等价地变成:

father1=1;

father2=1;

father3=1;

father4=1;

这样相当于把当前查询结点的路径上的所有结点的父亲都指向根结点,查找的时候就不需要一直回溯去找父亲了,查询的复杂度可以降为O(1)。

cpp 复制代码
int find(int i){
	int a=i;	//i在下面的while会变成根节点,保存一下
	while(i!=father[i]){	//找根节点
		i=father[i];
	}
	while(a!=father[a]){	//把路径上的所有节点改为根节点i
		int b=a;	//a要被其父节点覆盖,保存用来改变根节点
		a=father[a];
		father[b]=i;	//将原先的结点a的父亲改为根结点i
	}
	return i;	//返回根节点
}

递归:

cpp 复制代码
//写法一
int find(int i){
	if(i==father[i]){	//找根结点
		return i;	
	}else{
		int root=find(father[i]);	//递出寻找根节点
		father[i]=root;		//归回时赋予根节点root
		return root;
	}
}

//写法二
int find(int i){
	if(father[i]==i){
		return i;
	}
	return father[i]=find(father[i]);
	//朝着父节点一直递,直到父节点等于自己时归,归的时候将路径上的所有节点改为父节点
}

种类并查集(扩展域并查集)

并查集能维护连通性、传递性,通俗地说,亲戚的亲戚是亲戚

然而当我们需要维护一些对立关系 ,比如 敌人的敌人是朋友时,正常的并查集就很难满足我们的需求。

这时,种类并查集 (扩展域并查集)就诞生了。

常见的做法是将原并查集扩大一倍规模 ,并划分为两个种类:其中[1,n]表示处于一个种类,[n+1,2n]表示处于另一个种类。

同个种类 的并查集中合并,和原始的并查集没什么区别,仍然表达他们是朋友 这个含义。

考虑在不同种类 的并查集中合并的意义,其实就表达他们是敌人 这个含义了

按照并查集美妙的传递性,我们就能具体知道某两个元素到底是敌人还是朋友了。

有数据[1,n],种类并查集下假设[n+1,2n],现在处理数据x,y:

  • 维护朋友关系:

    • 合并x,y所在的树
  • 维护敌人关系:

    • x与y的假想敌合并(x与y的敌人是朋友)
    • y与x的假想敌合并(y与x的敌人是朋友)

题目

  1. 洛谷P3367
    模板并查集
    思路:
  • 纯纯板子题
  1. 洛谷P1525
    关押罪犯

思路:

  • 我们要尽可能让矛盾中值的罪犯在两个监狱里---矛盾值排序。敌人的敌人就是朋友:两个人a,b有仇,那么把他们放在一起显然会打起来,那么我们把a与b的其他敌人放在一起。

  • 首先需要并查集初始化

  1. 先把所有的矛盾关系按照矛盾值从大到小排一遍序

  2. 接下来每次操作取出一个关系,看矛盾的两个人x和y是否已经分配到同一个集合中(并查集找父亲即可)

    • 如果在一起那么显然会打起来(会出现矛盾),那么直接输出当前的边权(矛盾值)即可(此时可以保证是最小矛盾值,因为已经排序了)
    • 如果不在同一组,则按照"敌人的敌人就是朋友"的原则,把x与y的其他敌人分在同一组,y与x的其他敌人分在同一组
  3. 不断进行以上操作最终可以得到答案

相关推荐
木木子229 小时前
# 密码强度检测深度解析:正则表达式实时分析、多维度评分算法与可视化反馈
mysql·算法·华为·正则表达式·harmonyos
Sw1zzle11 小时前
算法入门(四):二叉树 - 递归遍历三件套
算法·leetcode
万法若空12 小时前
【算法-查找】查找算法
java·数据结构·算法
海石12 小时前
子树怎么找?树的3种遍历方式来帮忙!
算法·leetcode
海石12 小时前
难度分 1588:思路 + 技巧 = AC
算法·leetcode
殳翰12 小时前
下服务器端开发流程及相关工具介绍(C++)
开发语言·c++
青瓦梦滋13 小时前
协议定制/序列化-反序列化(Linux视角)
linux·服务器·网络·c++
珠海西格电力16 小时前
云边端协同架构:零碳园区管理系统的技术底座
大数据·运维·人工智能·算法·架构·能源
山登绝顶我为峰 3(^v^)317 小时前
C/C++ 交叉编译方法
java·c语言·c++
还有多久拿退休金17 小时前
让飞书知识库跟着 commit 自己长:一套自动化知识库的真实实现
前端·算法·架构