首页 > 其他分享 >【学习笔记】并查集

【学习笔记】并查集

时间:2023-07-25 22:24:05浏览次数:37  
标签:tx ty int 查集 笔记 学习 fa find

先来看百度百科上的定义:

并查集,在一些有N个元素的集合应用问题中,我们通常是在 开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。

并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。

并查集,顾名思义,并,合并;查,查找;集,集合;

普通并查集

先来看一个例题:P1551 亲戚。在这道题中,给出了一些亲戚的关系,又有几次询问询问两人是否为亲戚。这道题的重点在于:亲戚的亲戚也是亲戚

这就要用到我们的并查集了!我们令 \({fa}_i\) 为 \(i\) 的“上一辈”。我们定义,若 \({fa}_i = i\),则它为最终的祖先祖先。由于可能有很多独立的家族,所以可能有很多个祖先。

刚开始初始化 \(fa_i=i\),即每个人都是单独的祖先,对于每个亲戚关系 \(i\) 和 \(j\) 为亲戚,因为题目没有要求它们的大小关系,所以不妨假设 \(i\) 辈分比 \(j\) 小。对于亲戚的关系,若 \(i\) 和 \(j\) 为一个家族,那边没有合并的必要了。否则要合并两个家族,就必须从它们的祖先合并,那如何找到 \(i\) 和 \(j\) 的祖先呢?这里我们要用到递归的思想。

对于要查找到 \(x\) 的祖先,令 \(find(x)\) 为 \(x\) 的祖先,若 \(fa_x=x\),则自己为自己的祖先,则 \(find(x)=x\)。否则就要调用到他的上一辈:\(fa_x\)。显然 \(fa_x\) 的祖先也为 \(x\) 的祖先,所以若 \(fa_x\ne x\),则 \(find(x)=find(fa_x)\),这一部分用递归实现,代码如下:

int find(int x){
	if(x==fa[x])return x;
	else return find(fa[x]);
}

再回到上面合并的问题,对于两个人对应的两个家族合并,则需要先找到他们各自的祖先,令 \(ti\) 和 \(tj\) 分别为他们的祖先(\(ti=find(i),tj=find(j)\)),若他们不是是一个家族,也就是 \(ti\ne tj\),则不妨设 \(tj\) 的辈分比 \(ti\) 大,把 \(ti\) 归属到 \(tj\) 家族去,也就是 \(fa_{ti}=tj\),代码:

void merge(int x,int y){
    tx=find(x);ty=find(y);
    if(tx!=ty)fa[tx]=ty;
}

但注意到,其实 if(tx!=ty) 这个判断是可以省略的,因为若 \(tx=ty\),把他的祖先设为自己也没问题。

优化

当我们把上面的代码提交上去时,就会发现跑得很慢甚至会超时,这就要我们对其进行进一步的优化了。我们发现,时间主要是消耗在 \(find\) 上的,因为要递归调用很多层,所以会比较慢。那有没有办法使递归调用的层数小一点?我们便可以使用路径压缩。

怎么个压缩法呢?我们知道 \(find\) 函数的目的就是为了寻找祖先,那我们想要减少层数,就可以使 \(fa_x\) 直接为 \(x\) 的祖先,这样只用调用一层了。我们可以在前一次寻找的过程中不断地把经过的 \(x\) 都设为最终的的祖先,这样就快多了。其实这个优化实现非常简单,只需要在返回值时 return fa[x]=find(fa[x])就好了。

路径压缩就是我们最常用的优化了,实际上,还有一个小小的优化,我们知道,在合并后,若要寻找合并的家族的某些人时就要把这些人多调用一次,我们肯定是想多调用一次的人少,所以我们就在过程中不断统计每个家族的人数(记在家族祖先那),合并时把人少的的家族合并到人多的就可以了。这个优化叫做按秩合并,也叫启发式合并。

这些优化完完全全可以对付平常的并查集题了。

带边权并查集

带边权并查集与普通并查集相差不大,只是在普通并查集的基础上同时记录一个点到自己祖先节点的信息。

以这个例题为例:P1196。我们知道,如果只是要知道战舰是否在同一列就十分简单了,现在还要知道;两两战舰之间的战舰数便有些困难了。

我们令 \(d_x\) 为 \(x\) 到 \(fa_x\) 的距离,初始化为 \(0\),\(x\) 的祖先为 \(x\) 所在战舰的那一列的排头。在这基础上,在寻找祖先的过程中,在路径压缩之前,我们可以将这一路经过的距离累加起来并赋值给 \(d_x\),当 \(fa_x\) 等于 \(find(x)\) 时,这时的 \(d_x\) 便是 \(x\) 到 \(x\) 祖先的距离,在题目中的意思便是 \(x\) 到 \(x\) 所在列的排头的距离。

查询是比较简单的,那合并又怎么做?我们知道,并查集只能知道一个集合的祖先是谁,而不能知道一个集合中有的元素,这里是要求把 \(i\) 所在列连到 \(j\) 所在列,我们令 \(i'\) 为 \(i\) 列所在的头,\(j'\) 为 \(j\) 所在列的头,\(j''\) 为 \(j\) 所在列的尾。正常来说,我们是要把 \(i'\) 连到 \(j''\),但并查集不能查询尾,我们不妨换一个想法:既然连到尾不行,查询又之和 \(d_x\) 也就是 \(x\) 到头的距离,那我们直接把 \(i'\) 连到 \(j''\) 的头不就可以了吗?

但现在便有一个问题了。如果是连到尾的话,距离好算,但如果是连到头,\(d\) 数组不就乱套了吗?不急,我们先来看连到尾,先查询一遍 \(find()\),这时的 \(i'\) 到 \(j'\) 的距离为 \(1+d_{j''}\),换句话说,如果 \(i'\) 连到 \(j'\),要想 \(d\) 数组还是正确的,我们就得令 \(i'\) 到 \(j'\) 的距离也就是 \(d_{i'}\) 为 \(1+d_{j''}\),这时,你就会惊奇地发现,这道题就这样解决了!

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 30005;
int fa[N], d[N], siz[N];
int find(int x)
{
	if (x == fa[x]) return x;
	int t = find(fa[x]);
	d[x] += d[fa[x]];
	fa[x] = t;
	return t;
}
void combine(int x, int y)
{
	int tx = find(x), ty = find(y);
	fa[tx] = ty;
	d[tx] += siz[ty];
	siz[ty] += siz[tx];
}
int main()
{
	int T;
	cin >> T;
	for (int i = 1; i <= 30000; i++)
		fa[i] = i, siz[i] = 1;
	while (T --)
	{
		char op;
		int x, y;
		cin >> op >> x >> y;
		if (op == 'M') combine(x, y);
		else
		{
			if (find(x) != find(y)) cout << -1 << endl;
			else cout << abs(d[x] - d[y]) - 1 << endl;
		}
	}
	return 0;
}

扩展域并查集

有的时候,需要维护更多的信息时,我们可以把并查集“扩展”开来。

例题:CF766D。在这题中,我们不仅要维护同义词,还要维护反义词,我们就可以把并查集“扩展”出一个域来,其中 \(1,2,\cdots,n\) 维护同义词,\(n+1,n+2,\cdots,n+n\) 维护反义词。简单来说,对于第 \(i\) 个单词,\(i\) 是记录它的同义词的关系的,而 \(i+n\) 是记录反义关系的。

我们先来考虑合并。令 \(i\) 和 \(j\) 为一组单词。若它们为同义词,我们根据常识知道:

  • 朋友的朋友还是朋友 \(\rightarrow\) 同义词的同义词还是同义词 \(\rightarrow\) merge(i,j)
  • 敌人的敌人是朋友 \(\rightarrow\) 同义词的同义词还是同义词 \(\rightarrow\) merge(i+n,j+n)

若它们为反义词:

  • 朋友的敌人是敌人 \(\rightarrow\) 同义词的反义词是同义词 \(\rightarrow\) merge(i+n,j)
  • 敌人的朋友还是敌人 \(\rightarrow\) 反义词的同义词还是反义词 \(\rightarrow\) merge(i,j+n)

同理,对于查询,如果 \(find(i)=find(j)\) 或 \(find(i+n)=find(j+n)\) 则为同义,否则若 \(find(i+n)=find(j)\) 或 \(find(i)=find(j+n)\) 则为反义,否则为没有关系。代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
map<string, int> p;
int fa[N];
int find(int x)
{
	if (x == fa[x]) return x;
	else return fa[x] = find(fa[x]);
}
int main()
{
	int n, m, k;
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++)
	{
		string x;
		cin >> x;
		p[x] = i;
		fa[i] = i; fa[i + n] = i + n;
	}
	while (m--)
	{
		int op;
		string x, y;
		cin >> op >> x >> y;
		int tx = p[x], ty = p[y];
		int t1 = find(tx), t2 = find(tx + n);
		int t3 = find(ty), t4 = find(ty + n);
		if (op == 1)
		{
			if (t1 == t4 || t2 == t3) cout << "NO" << endl;
			else fa[t1] = t3, fa[t2] = t4, cout << "YES" << endl;
		}
		else
		{
			if (t1 == t3 || t2 == t4) cout << "NO" << endl;
			else fa[t2] = fa[t3], fa[t1] = fa[t4], cout << "YES" << endl;
		}
	}
	while (k--)
	{
		string x, y;
		cin >> x >> y;
		int tx = p[x], ty = p[y];
		if (find(tx) == find(ty) || find(tx + n) == find(ty + n)) cout << 1 << endl;
		else if (find(tx + n) == find(ty) || find(tx) == find(ty + n)) cout << 2 << endl;
		else cout << 3 << endl;
	}
	return 0;
}

标签:tx,ty,int,查集,笔记,学习,fa,find
From: https://www.cnblogs.com/zhuoyuxuan/p/17581200.html

相关文章

  • (数据科学学习手札153)基于martin的高性能矢量切片地图服务构建
    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes1简介大家好我是费老师,在日常研发地图类应用的场景中,为了在地图上快速加载大量的矢量要素,且方便快捷的在前端处理矢量的样式,且矢量数据可以携带对应的若干属性字段,目前主流的做法......
  • 7.24 树上问题2笔记
    题单T1题目•有一棵点数为\(n\)的树。•有\(q\)次询问,每次询问有多少个点到\(a,b\)距离相等。•\(1≤n\),\(q≤500000\)。Solution•设询问\(a,b\)两点直接的路长度为\(d\)。•如果\(d\)为奇数,那么无解,\(d\)为偶数有解。•考虑以下几种情况:\(......
  • rce漏洞学习
    rce漏洞:利用代码或者命令去执行常见的一句话木马:1、普通一句话:123<?php@eval($_POST[123456]);?>*post后面中括号里面的内容是使用菜刀或蚁剑连接时的密码2、防爆破一句话:1234<?phpsubstr(md5($_REQUEST['x']),28)=='6862'&&eval($_REQU......
  • python导入机器学习包
    如何在Python中导入机器学习包作为一名经验丰富的开发者,你对Python编程语言和机器学习都非常熟悉。现在有一位刚入行的小白不知道如何在Python中导入机器学习包,你需要教会他。在本篇文章中,我将向你介绍整个导入机器学习包的流程,并提供每个步骤所需的代码和对代码的注释。导入机器......
  • 7.23 树上问题笔记
    题单由于题目过多,只放几道重要的。。。T1题目•A国有\(n\)座城市,编号从\(1\)到\(n\),城市之间有\(m\)条双向道路。每一条道路对车辆都有重量限制\(z\),简称限重。•现在有\(q\)辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。......
  • 【学习笔记】无向图的连通性
    #割点**定义:**在无向图连通图中,若把点$x$删去后整个图就不连通了,则$x$为割点(割顶)。**朴素方法:**每次删去一个点,然后判断图是否连通,时间复杂度为$O(n(n+m))$。**Tarjan算法:**$dfn_x$:$x$被`dfs`到的时间戳$low_x$:在$x$及以后被搜索的所有节点的$low$值和这些节......
  • chatgpt从入门到精通深入学习路线?
    chatgpt从入门到精通深入学习路线?如果您想深入学习和掌握ChatGPT,以下是一个学习路线的建议:1.了解自然语言处理(NLP)基础知识:开始之前,建议您对NLP的基本概念和技术有所了解,包括语言模型、分词、词向量、文本分类等。2.学习深度学习和神经网络:ChatGPT是基于深度学习技术的,因此了......
  • JDK11~19 从入门到精通进阶学习路线?
    JDK11~19从入门到精通进阶学习路线?学习JDK的进阶路线可以按照以下步骤进行:1.理解基础概念和语法:首先,你需要对Java语言的基本概念和语法有一定的了解。学习Java的入门资料、教程或者参加培训课程都是一个好的方式。2.学习面向对象编程(OOP):Java是一种面向对象的编程语言,掌握面......
  • 笔记-交易圣经
    笔记-交易圣经通用原则一:思想准备最大逆境:没有最坏,只有更坏情绪指向:目标与期望失利:失败是必然随机市场:不确定性,不可预测性输得起才会赢:生存是第一要义风险管理:如上交易伙伴:防止自欺欺人财务边界:闲钱通用原则二:启蒙避免爆仓风险:有可能即是迟早会发生。没有杠杆减......
  • 23-7-14学习记录
    1.volatile的作用volatile关键字有作用是确保被修饰的变量在多线程环境下的可见性和有序性。可见性(Visibility):当一个变量被声明为volatile时,它的修改对其他线程是可见的。这意味着当一个线程修改了一个volatile变量的值,其他线程能够立即看到最新的值,而不是使用缓存中的旧值。这......