热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

算法竞赛专题解析(3):并查集

本系列是这本算法教材的扩展:《算法竞赛入门到进阶》(京东当当)清华大学出版社本文web地址:PDF下载地址:其中的“补充资料”如有建议,请联系:(1)QQ群,567554289;(


目录




  • 0 并查集简介

  • 1 并查集的基本操作

  • 2 合并的优化

  • 3 查询的优化——路径压缩

  • 4 带权并查集

    • 4.1 带权值的路径压缩和合并

    • 4.2 例题


  • 5 习题

  • 6 参考文献



本系列是这本算法教材的扩展:《算法竞赛入门到进阶》(京东 当当) 清华大学出版社

本文web地址:https://blog.csdn.net/weixin_43914593

PDF下载地址:https://github.com/luoyongjun999/code 其中的“补充资料”

如有建议,请联系:(1)QQ 群,567554289;(2)作者QQ,15512356

??本篇包括:

??(1)1~3节,是《算法竞赛入门到进阶》书中原有的内容。

??(2)4节“带权并查集”,是扩展内容。


0 并查集简介

??并查集(Disjoint Set)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。经典的应用有:连通子图、最小生成树Kruskal算法[ 参考本书第10章“10.10.2 kruskal算法”。]和最近公共祖先(Least Common Ancestors, LCA)等。

??并查集在算法竞赛中极为常见。

??通常用“帮派”的例子来说明并查集的应用背景。一个城市中有n个人,他们分成不同的帮派;给出一些人的关系,例如1号、2号是朋友,1号、3号也是朋友,那么他们都属于一个帮派;在分析完所有的朋友关系之后,问有多少帮派,每人属于哪个帮派。给出的n可能是10^6^的。

??读者可以先思考暴力的方法,以及复杂度。如果用并查集实现,不仅代码很简单,而且复杂度可以达到O(logn)。

??并查集:将编号分别为1~n的n个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。在这个集合中,并查集的操作有:初始化、合并、查找。

??本文比较全面地介绍了并查集:

??(1)并查集的基本操作。

??(2)并查集的优化:合并和路径压缩。

??(3)带权并查集。

??并查集的基本应用是集合问题;加上权值之后,利用并查集的合并和查询优化,可以对权值所代表的具体应用进行高效的操作。


1 并查集的基本操作

??(1)初始化。定义数组int s[]是以结点i为元素的并查集,开始的时候,还没有处理点与点之间的朋友关系,所以每个点属于独立的集,并且以元素i的值表示它的集s[i],例如元素1的集s[1]=1。

??下面是图解,左边给出了元素与集合的值,右边画出了逻辑关系。为了便于讲解,左边区分了结点i和集s:把集的编号加上了下划线;右边用圆圈表示集,方块表示元素。


技术分享图片

图1 并查集的初始化

??(2)合并,例如加入第一个朋友关系(1, 2)。在并查集s中,把结点1合并到结点2,也就是把结点1的集1改成结点2的集2。

技术分享图片


图2 合并(1, 2)


??(3)合并,加入第二个朋友关系(1, 3)。查找结点1的集,是2,再递归查找元素2的集是2,然后把元素2的集2合并到结点3的集3。此时,结点1、2、3都属于一个集。右图中,为简化图示,把元素2和集2画在了一起。

技术分享图片


图3 合并(1, 3)


??(4)合并,加入第三个朋友关系(2, 4)。结果如下,请读者自己分析。

技术分享图片


图4 合并(2, 4)


??(5)查找。上面步骤中已经有查找操作。查找元素的集,是一个递归的过程,直到元素的值和它的集相等,就找到了根结点的集。从上面的图中可以看到,这棵搜索树的高度,可能很大,复杂度是O(n)的,变成了一个链表,出现了树的“退化”现象。

??(6)统计有多少个集。如果s[i] = i,这是一个根结点,是它所在的集的代表;统计根结点的数量,就是集的数量。

?例题

??下面以hdu 1213为例子,实现上述操作。http://acm.hdu.edu.cn/showproblem.php?pid=1213

??有n个人一起吃饭,有些人互相认识。认识的人想坐在一起,而不想跟陌生人坐。例如A认识B,B认识C,那么A、B、C会坐在一张桌子上。

??给出认识的人,问需要多少张桌子。

??一张桌子是一个集,合并朋友关系,然后统计集的数量即可。下面的代码是并查集操作的具体实现。

#include
using namespace std;
const int maxn = 1050;
int s[maxn];
void init_set(){ //初始化
for(int i = 1; i <= maxn; i++)
s[i] = i;
}
int find_set(int x){ //查找
return x==s[x]? x:find_set(s[x]);
}
void merge_set(int x, int y){ //合并
x = find_set(x);
y = find_set(y);
if(x != y) s[x] = s[y]; //把x合并到y上,y的根成为x的根
}
int main (){
int t, n, m, x, y;
cin >> t;
while(t--){
cin >> n >> m;
init_set();
for(int i = 1; i <= m; i++){
cin >> x >> y;
merge_set(x, y);
}
int ans = 0;
for(int i = 1; i <= n; i++) //统计有多少个集
if(s[i] == i)
ans++;
cout < }
return 0;
}

??复杂度:上述程序,查找find_set()、合并merge_set()的搜索深度是树的长度,复杂度都是O(n),性能比较差。下面介绍合并和查询的优化方法,优化之后,查找和合并的复杂度都小于O(logn)。


2 合并的优化

??合并元素x和y时,先搜到它们的根结点,然后再合并这两个根结点,即把一个根结点的集改成另一个根结点。这两个根结点的高度不同,如果把高度较小的集合并到较大的集上,能减少树的高度。下面是优化后的代码,在初始化时用height[i]定义元素i的高度,在合并时更改。

int height[maxn];
void init_set(){
for(int i = 1; i <= maxn; i++){
s[i] = i;
height[i]=0; //树的高度
}
}
void merge_set(int x, int y){ //优化合并操作
x = find_set(x);
y = find_set(y);
if (height[x] == height[y]) {
height[x] = height[x] + 1; //合并,树的高度加一
s[y] = x;
}
else{ //把矮树并到高树上,高树的高度保持不变
if (height[x] else s[y] = x;
}
}

3 查询的优化——路径压缩

??在上面的查询程序find_set()中,查询元素i所属的集,需要搜索路径找到根结点,返回的结果是根结点。这条搜索路径可能很长。如果在返回的时候,顺便把i所属的集改成根结点,那么下次再搜的时候,就能在O(1)的时间内得到结果。

技术分享图片


图5 路径压缩

??程序如下:

int find_set(int x){
if(x != s[x])
s[x] = find_set(s[x]); //路径压缩
return s[x];
}

?? 这个方法称为路径压缩,因为整个搜索路径上的元素,在递归过程中,从元素i到根结点的所有元素,它们所属的集都被改为根结点。路径压缩不仅优化了下次查询,而且也优化了合并,因为合并时也用到了查询。

??上面代码用递归实现,如果数据规模太大,担心爆栈,可以用下面的非递归代码:

int find_set(int x){
int r = x;
while ( s[r] != r ) r=s[r]; //找到根结点
int i = x, j;
while(i != r){
j = s[i]; //用临时变量j记录
s[i]= r ; //把路径上元素的集改为根结点
i = j;
}
return r;
}

4 带权并查集

??前面讲解了并查集的基本应用:处理集合问题。并查集的高效,主要是利用了合并和查询的优化。在这些基本应用中,点之间只有简单的归属关系,而没有权值。如果在点之间加上权值,并查集的应用会更广泛。

??如果读者联想到树这种数据结构,会发现,并查集实际上是在维护若干棵树。并查集的合并和查询优化,实际上是在改变树的形状,把原来“细长”的、操作低效的大量“小树”,变成了“粗短”的、操作高效的少量“大树”。如果在原来的“小树”上,点之间有权值,那么经过并查集的优化之变成“大树”后,这些权值的操作也变得高效了。


4.1 带权值的路径压缩和合并

??定义一个权值数组d[],结点i到父结点的权值为记为d[i]。

??(1)带权值的路径压缩

?? 下面的图,是加上权值之后的路径压缩。原来的权值d[],经过压缩之后,更新为d[]‘,例如d[1]‘=d[1]+d[2]+d[3]。

?? 需要注意的是,这个例子中,权值是相加的关系,比较简单;在具体的题目的中,可能有相乘、异或等等符合题意的操作。

技术分享图片


图6 带权值的路径压缩

??相应地,在这个权值相加的例子中,把路径压缩的代码改为:

int find_set(int x){
if(x != s[x]) {
int t = s[x]; //记录父结点
s[x] = find_set(s[x]); //路径压缩。递归最后返回的是根结点
d[x] += d[t]; //权值更新为x到根节点的权值
}
return s[x];
}

??注意代码中的细节。原来的d[x]是点x到它的父结点的权值,经过路径压缩后,x直接指向根节点,d[x]也更新为x到根结点的权值。这是通过递归实现的。

??代码中,先用t记录x的原父结点;在递归过程中,最后返回的是根节点;最后将当前节点的权值加上原父结点的权值(注意:经过递归,此时父结点也直接指向根节点,父结点的权值也已经更新为父结点直接到根结点的权值了),就得到当前节点到根节点的权值。

??(2)带权值的合并

?? 在合并操作中,把点x与到点y合并,就是把x的根结点fx合并到y的根结点fy。在fx和fy之间增加权值,这个权值要符合题目的要求。


4.2 例题

?? 下面用2个经典例题讲解带权并查集,hdu 3038和poj 1182。

??(1)例题1:hdu 3038

?问题描述

??给出区间[a, b],区间之和为v。输入m组数据,每输入一组,判断此组条件是否与前面冲突,最后输出与前面冲突的数据的个数。比如先给出[1, 5]区间和为100,再给出区间[1, 2]的和为200,肯定有冲突。

?题解

??本题是本节讲解的带权值并查集的直接应用。如果能想到可以把序列建模为并查集,就能直接套用模板了。

#include
using namespace std;
const int maxn =200010;
int s[maxn]; //集合
int d[maxn]; //权值:记录当前结点到根结点的距离
int ans;
void init_set(){ //初始化
for(int i = 0; i <= maxn; i++)
{ s[i] = i; d[i] = 0; }
}
int find_set(int x){ //带权值的路径压缩
if(x != s[x]) {
int t = s[x]; //记录父结点
s[x] = find_set(s[x]); //路径压缩。递归最后返回的是根结点
d[x] += d[t]; //权值更新为x到根节点的权值
}
return s[x];
}
void merge_set(int a, int b,int v){ //合并
int roota = find_set(a), rootb = find_set(b);
if(roota == rootb){
if(d[a] - d[b] != v)
ans++;
}
else{
s[roota] = rootb; //合并
d[roota] = d[b]- d[a] + v;
}
}
int main(){
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
init_set();
ans = 0;
while(m--){
int a,b,v;
scanf("%d%d%d",&a,&b,&v);
a--;
merge_set(a, b, v);
}
printf("%d\n",ans);
}
return 0;
}

??(2)例题2:poj 1182 食物链 http://poj.org/problem?id=1182

?问题描述

??动物王国中有三类动物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(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。

?题解

??这一题中的权值比较有趣,它不是上一题中相加的关系。把权值d[]记录为两个动物在食物链上的相对关系。下面用d(A->B)表示A、B的关系,d(A->B) = 0表示同类,d(A->B) = 1表示A吃B,d(A->B) = 2表示A被B吃。

??这一题难点在权值的更新。考虑三个问题:

??(i)路径压缩时,如何更新权值。

??若d(A->B) =1,d(B->C) = 1,求d(A->c)。因为A吃B,B吃C,那么C应该吃A,得d(A->C)=2;

??若d(A->B) =2,d(B->C) =2,求d(A->c)。因为B吃A,C吃B,那么A应该吃C,得d(A->C)=1;

??若d(A->B) = 0,d(B->C) =1,求d(A->c)。因为A、B同类,B吃C,那么A应该吃C,得d(A->C)=1;

??找规律知:d(A->C) = (d(A->B) + d(B->C) ) % 3,因此关系值的更新是累加再模3。

??(ii)合并时,如何更新权值。本题的权值更新是取模操作,内容见下面的代码。

??(iii)如何判断矛盾。如果已知A与根节点的关系,B与根节点的关系,如何求A、B之间的关系?内容见下面的代码。

??下面是代码。

#include
#include
using namespace std;
const int maxn = 50005;
int s[maxn]; //集合
int d[maxn]; // 0:同类; 1:吃; 2:被吃
int ans;
void init_set(){ //初始化
for(int i = 0; i <= maxn; i++)
{ s[i] = i; d[i] = 0; }
}
int find_set(int x){ //带权值的路径压缩
if(x != s[x]) {
int t = s[x]; //记录父结点
s[x] = find_set(s[x]); //路径压缩。递归最后返回的是根结点
d[x] = (d[x] + d[t]) % 3; //权值更新为x到根节点的权值
}
return s[x];
}
void merge_set(int x, int y, int relation){ //合并
int rootx = find_set(x);
int rooty = find_set(y);
if (rootx == rooty){
if ((relation - 1) != ((d[x] - d[y] + 3) % 3)) //判断矛盾
ans++;
}
else {
s[rootx] = rooty; //合并
d[rootx] = (d[y] - d[x] + relation - 1) % 3; //更新权值
}
}
int main(){
int n, k; cin >> n >> k;
init_set();
ans = 0;
while (k--){
int relation, x, y;
scanf("%d%d%d",&relation,&x,&y);
if ( x > n || y > n || (relation == 2 && x == y ) )
ans++;
else
merge_set(x,y,relation);
}
cout < return 0;
}

5 习题

poj 2524 Ubiquitous Religions,并查集简单题。

poj 1611 The Suspects,简单题。

poj 1703 Find them, Catch them。

poj 2236 Wireless Network。

poj 2492 A Bug‘s Life。

poj 1988 Cube Stacking。

poj 1182食物链,经典题。

hdu 3635 Dragon Balls。

hdu 1856 More is better。

hdu 1272 小希的迷宫。

hdu 1325 Is It A Tree。

hdu 1198 Farm Irrigation。

hdu 2586 How far away,最近公共祖先,并查集+深搜。

hdu 6109 数据分割,并查集+启发式合并。


6 参考文献

poj 1182的不同解法,参考[1][2]:

[1]《算法竞赛进阶指南》李煜东,河南电子音像出版社,用“扩展域”的并查集求解poj1182。

[2]《挑战程序设计竞赛》秋叶拓哉,人民邮电出版社,用普通并查集求解poj1182。

[3]leetcode上有一个并查集题目集:https://leetcode-cn.com/tag/union-find/


推荐阅读
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 本文介绍了在Python中使用zlib模块进行字符串的压缩与解压缩的方法,并探讨了其在内存优化方面的应用。通过压缩存储URL等长字符串,可以大大降低内存消耗,虽然处理时间会增加,但是整体效果显著。同时,给出了参考链接,供进一步学习和应用。 ... [详细]
  • 本文介绍了响应式页面的概念和实现方式,包括针对不同终端制作特定页面和制作一个页面适应不同终端的显示。分析了两种实现方式的优缺点,提出了选择方案的建议。同时,对于响应式页面的需求和背景进行了讨论,解释了为什么需要响应式页面。 ... [详细]
  • 本文内容为asp.net微信公众平台开发的目录汇总,包括数据库设计、多层架构框架搭建和入口实现、微信消息封装及反射赋值、关注事件、用户记录、回复文本消息、图文消息、服务搭建(接入)、自定义菜单等。同时提供了示例代码和相关的后台管理功能。内容涵盖了多个方面,适合综合运用。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
author-avatar
龙井龙井2502908921
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有