我知道并查集的可爱之处

迈入门槛

并查集是一种可以动态维护若干个不重叠的集合,并支持合并和查询的数据结构。注:这里的动态维护应该理解成可以随时修改的意思。

并查集,并查集,顾名思义,就是可以合并和查询若干个集合。

文字概念

下面是一些基本操作和思想(部分摘自于并查集的百度百科):

  1. 在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,这个元素就是集合的代表元素。【代表元法初始化】
  2. 可以按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。【基本操作合并,查询】
  3. 集合问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。【地位】
  4. 并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。【所属的类别】

文字复杂看不懂??那接下来我们图示!
(转载于 https://blog.csdn.net/niushuai666/article/details/6662911) 【小字为博主注释,所有的代码都是博主自己的代码】

故事配图辅助理解

故事读完,并查集就会了~~~~~

江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。

这样一来,江湖上就形成了一个一个的帮派这里的帮派就是上面的集合),通过两两之间的朋友关系串联起来合并操作)。而不在同一个帮派的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?

我们可以在每个朋友圈内推举出一个比较有名望的人代表元初始化),作为该圈子的代表人物。这样,每个圈子就可以这样命名“中国同胞队”美国同胞队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。

但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长抓狂要判断自己的队长是谁,只能漫无目的这里可以想一下怎么优化)的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样,想打一架得先问个几十年,饿都饿死了,受不了。这样一来,队长面子上也挂不住了,不仅效率太低,还有可能陷入无限循环中。

于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构与上面的第四点不相吻合),我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层在此可以考虑一下优化,能不能更快捷一点??),就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否是一个帮派的,至于他们是如何通过朋友关系相关联的,以及每个圈子内部的结构是怎样的,甚至队长是谁,都不重要了。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。

在这里插入图片描述
下面我们来看并查集的实现。

int fa[1000]; 这个数组,记录了每个大侠的上级是谁。大侠们从1或者0开始编号(依据题意而定),fa[15]=3就表示15号大侠的上级是3号大侠。如果一个人的上级就是他自己,fa[i]=i,那说明他就是掌门人了,查找到此为止。

也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。每个人都只认自己的上级。比如胡青牛同学只知道自己的上级是杨左使。张无忌是谁?不认识!要想知道自己的掌门是谁,只能一级级查上去。

find这个函数就是找掌门用的,意义再清楚不过了(路径压缩算法(优化算法)先不论,后面再说)。

int gf(int x) //查找根结点,getfather
{
	int son, t;
	son = x;
	while(x != fa[x]) //我的上级不是掌门
		x = fa[x];
	while(son != x) //我就找他的上级,直到掌门出现
	{
		t = fa[son];
		fa[son] = x;
		son = t;
	}
	return x; //掌门驾到~~
}

再来看看merge函数,就是在两个点之间连一条线,这样一来,原先它们所在的两个板块的所有点就都可以互通了。这在图上很好办,画条线就行了。

但我们现在是用并查集来描述武林中的状况的,一共只有一个 fa[] 数组,该如何实现呢? 还是举江湖的例子,假设现在武林中的形势如图所示。虚竹帅锅与周芷若MM是我非常喜欢的两个人物,他们的终极boss分别是玄慈方丈和灭绝师太,那明显就是两个阵营了。我不希望他们互相打架,就对他俩说:“你们两位拉拉勾,做好朋友吧。”他们看在我的面子上,同意了。这一同意可非同小可,整个少林和峨眉派的人就不能打架了。

这么重大的变化,可如何实现呀,要改动多少地方?其实非常简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先的所有人员的终极boss都是师太,那还打个球啊!反正我们关心的只是连通性,门派内部的结构不要紧的。”玄慈一听肯定火大了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”于是,两人相约一战,杀的是天昏地暗,风云为之变色啊,但是啊,这场战争终究会有胜负,胜者为王。弱者就被吞并了。反正谁加入谁效果是一样的,门派就由两个变成一个了。这段函数的意思明白了吧?

void merge(int x, int y) //虚竹和周芷若做朋友
{
	int xx, yy;
	xx = gf(root1);//我老大是玄慈
	yy = gf(root2);//我老大是灭绝
	if(xx != yy) 
		fa[xx] = yy; //打一仗,谁赢就当对方老大
}

再来看看路径压缩算法

建立门派的过程是用merge函数两个人两个人地连接起来的,谁当谁的手下完全随机。最后的树状结构会变成什么样,我也无法预知,一字长蛇阵也有可能。这样查找的效率就会比较低下最理想的情况就是所有人的直接上级都是掌门,一共就两级结构,只要找一次就找到掌门了。哪怕不能完全做到,也最好尽量接近。这样就产生了路径压缩算法。

设想这样一个场景:两个互不相识的大侠碰面了,想知道能不能干一场。 于是赶紧打电话问自己的上级:“你是不是掌门?” 上级说:“我不是呀,我的上级是谁谁谁,你问问他看看。” 一路问下去,原来两人的最终boss都是东厂曹公公。 “哎呀呀,原来是自己人,有礼有礼,在下三营六组白面葫芦娃!” “幸会幸会,在下九营十八组仙子狗尾巴花!” 两人高高兴兴地手拉手喝酒去了。(现在是知道了自己的顶头上司是曹公公)

“等等等等,两位大侠请留步,还有事情没完成呢!”我叫住他俩。 “哦,对了,还要做路径压缩。”两人醒悟。 白面葫芦娃打电话给他的上级六组长:“组长啊,我查过了,其实偶们的掌门是曹公公。不如偶们一起结拜在曹公公手下吧,省得级别太低,以后查找掌门麻烦。” “唔,有道理。” 白面葫芦娃接着打电话给刚才拜访过的三营长……仙子狗尾巴花也做了同样的事情。

这样,查询中所有涉及到的人物都聚集在曹公公的直接领导下。每次查询都做了优化处理,所以整个门派树的层数都会维持在比较低的水平上。路径压缩的代码,看得懂很好,看不懂可以自己模拟一下,很简单的一个递归而已。总之它所实现的功能就是这么个意思。
在这里插入图片描述

于是,问题圆满解决。。。。。。。。。

代码如下:

#include<bits/stdc++.h>
using namespace std;

int n,m;//一共有n个门派,m条关系 
int cnt=0;//统计m条关系之后还剩下多少个门派
int fa[1010]; //里面全是掌门 

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}
 
inline int gf(int x)
{
	if(fa[x]==x)	return x;//掌门是自己本身,return掌门 
	return fa[x]=gf(fa[x]);//路径压缩寻找掌门 
} 
 
int main()
{
	n=read(), m=read();
	for(int i=1;i<=n;++i)	fa[i]=i;//代表元初始化,每个人都是自己的掌门 
	for(int i=1;i<=m;++i)
	{
		int x=read(), y=read();//两个大侠要做好盆友 
		int xx=gf(x), yy=gf(y);//相当于上面的merge操作
		fa[xx]=yy;//掌门结拜 
	} 
	for(int i=1;i<=n;++i)
		if(gf(i)==i)
			cnt++; 
	
	printf("%d\n",cnt); 
	return 0;
}

以上是并查集的所有基本操作。。。


浅尝辄止

之后我们就拿上几道题上演与并查集的 唯美邂逅

畅通工程

因为这是第一道题,所以我们重点讲一下。

题目大意:给m条两两联通的路,求还需要多少条路使得n个点形成一个连通块。

解题报告

  1. 概念迁移:可以看成是一开始有互不关联的n个集合,之后给出若干条关系,判断还需要多少条关系,使得n个集合合成一个集合。
  2. 模型套用:那么不就和上面讲的给你n个掌门,给你m条关系,求m条关系后,还剩多少个掌门(假设剩余ans个)是一样的啊!
  3. 举一反三:题中问的是还需要多少个关系,使得n个集合合成一个集合,那么和模型不一样的在哪呢?模型给出的是最后还剩几个集合,那么再加上一步,求出ans个集合合并成一个集合的步数即可,显然答案是ans-1。

注意
HDU不可以用万能库
要学会上面标黑的解题思路

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int nn=1010;

int n,m;
int fa[nn];

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

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

int main()
{
	while(cin>>n>>m)
	{
		if(n==0)	break;
		for(int i=1;i<=n;++i)	fa[i]=i;
		for(int i=1;i<=m;++i)
		{
			int x=read();
			int y=read();
			int xx=gf(x);
			int yy=gf(y);
			if(xx==yy)	continue;
			fa[xx]=yy;
		}
		int cnt=0;
		for(int i=1;i<=n;++i)
			if(gf(i)==i)
				cnt++;
		printf("%d\n",cnt-1);
	}
	return 0;
} 

程序自动分析

解题报告:给出若干条关系,运用并查集判断是否成立即可。

自己一开始是建图的思路,判断是否连通。
建图错因

  1. 没有初始化。。。(初始化了n多次,要么没初始化完全,要么是初始化的位置不对。。)
  2. 建图的时候建成了单向边,应该是双向边的;
  3. 思路错误。应该是把所有的e=1的存起来,之后再去判定内些e=0的条件。
    我问了一下startaidou大佬,这样一来就是离线的作法,但是实现代码非常困难,所以并查集是明显优于建图的。
  4. 最后还仅仅只是将建图优化到了40分。。。
  5. 可能是因为建图的复杂度过于高,所以就死活过不了。

之后的并查集的思路:

把相等关系的数字合并到一个集合里;
之后对于要求不相等的数字就判断他们在不在一个集合内;
如果在,就不满足条件,否则,就满足条件。

难点
离散化:lower_bound(t+1,t+1+m , x);
int m=unique(t+1,t+1+n)-(t+1);

再打代码时出现的问题

  1. 在使用unique的时候没有排序;
  2. lower_bound的格式不对:lower_bound(a+1,a+1+m,x)-a;
  3. 在lower_bound()函数中数组大小应该是你去重之后的函数大小,如果之前你有去重的话;
  4. 书写代码的时候不够规范,发现在 if(gf(find(m[i].x))==gf(find(m[i].y)));后面居然有一个分号!!!!!!!【就可以说明代码是非常不规范了

小技巧:
离散化的时候最主要的就是unique函数和lower_bound()函数
格式: m=unique(a+1,a+n+1)-(a+1);
int x=lower_bound(a+1,a+1+m,x)-a;

并查集的代码:

#include<bits/stdc++.h>
using namespace std;

const int nn=1e5+100;

int t,n;
int fa[nn*2];
int a[nn*2];
int m; 

struct contect
{
	int x,y,e;
}c[nn];

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

int find(int x)
{
	return lower_bound(a+1,a+m+1,x)-a;
}

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

int main()
{
	t=read();
	while(t--)
	{
		n=read(); bool flag=true;
		for(int i=1;i<=n;++i)
		{
			c[i].x=read();
			c[i].y=read();
			c[i].e=read();
			a[i*2-1]=c[i].x;
			a[i*2]=c[i].y;
		}
		sort(a+1,a+2*n+1); 
		/*问题1:没有排序*/
		m=unique(a+1,a+2*n+1)-(a+1);/*集合中的元素被压缩成了m个*/
		for(int i=1;i<=m;++i)	fa[i]=i;
		for(int i=1;i<=n;++i)
			if(c[i].e)
				fa[gf(find(c[i].x))]=gf(find(c[i].y));
		for(int i=1;i<=n;++i)
			if(!c[i].e&&gf(find(c[i].x))==gf(find(c[i].y)))
				flag=false;
		if(flag)	printf("YES\n");
		else		printf("NO\n");
	}
	return 0;
} 

supermarket

一开始就wa了…

错因
应该是fa[]的初始化出现了问题;应该把fa数组中的每一个值都赋值为它本身,因为在天数范围内的都算是一个可行的状态。

带权并查集:P带权并查集合并的是可以推算关系的点的集合,就类似于是把并查集这种类似与树的数据结构加上边权。

code

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int nn=10010;

int n;
int ans=0,maxx=0;
struct goods
{
	int p,d;
}g[nn];
int fa[nn];

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

bool mycmp(goods x,goods y)
{
	return x.p>y.p;
}

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

int main()
{
	while(cin>>n)
	{
		memset(fa,0,sizeof(fa));
		ans=0;
		for(int i=1;i<=n;++i)
		{
			g[i].p=read();
			g[i].d=read();
			maxx=max(maxx,g[i].d);
		}
		for(int i=1;i<=maxx;++i)	fa[i]=i;
		sort(g+1,g+n+1,mycmp);
		for(int i=1;i<=n;++i)
		{
			int xx=gf(g[i].d);
			if(xx<=0)	continue;
			fa[xx]=xx-1;
			ans+=g[i].p;
		}
		printf("%d\n",ans);
	}
	return 0;
}

这些就是一些比较基础的数据结构的操作,希望要勤加练习。


慢慢深入

之后就是要讲一下“带边权”并查集并查集的“扩展域”

首先从字面上理解这两个词语:

  1. “带边权”并查集:顾名思义,并查集带上了边权,那么此时就会在gf操作中增加一个累积该节点到祖先的边权的操作(这里的d[]是边权的累计):
int gf(int x)
{
	if(x==fa[x])	return x;
	int root=gf(fa[x]);
	d[x]+=d[fa[x]];
	return fa[x]=root;
}
  1. 并查集的“扩展域”:望文生义,扩展域不就是比原来多了几个存储并查集的地方,原来的并查集只有数字(x)本身与(y)本身的关系,之后就可能变成了x与y的衍生区域的关系(x1与y1),(x2与y2),……

如果还是不能理解的话,我们就可以看几道题目深入理解一下!

银河英雄传说

在这道题之中,求得不仅是两个集合的所属关系,而且求的是两个合并之后的的集合的距离,那么这个距离怎么求??

这样就可以类比一下图中的一些操作,对于没有加边权的图,表示的只是两个点的连通,或者是两个东西之间的关系,就像是并查集一样,仅仅表示的就是两个集合的所属关系。

但是,加边权之后呢,这个图就不仅可以表示出来两个事物之间的关系,增加的是可以描述两个事物之间的距离

所以在这道题中,我们就可以采取同样的方法,为并查集加边权以此达到求解两个事物之间的距离的目的。

上面已经说过了,加边权的部分改动,之后就应该自己想一下要怎么实现??

**一开始得到了0分。。**但是,我没调之前的代码,估计又是什么sb的代码细节,之后又打一遍就过掉了。

错因

  1. 一开始初始化的问题:d[]=1,size[]=0,但实际上这样做是反的,因为d[x]记录的是在x之前的点数,所以一开始就应该赋值为0,反而size[x]记录的是当前x所在列的长度,初始化的时候,每一个点都属于自己的一列,那么初始化的时候就应该是size[x]=1;
  2. 在gf函数中的路径压缩中要记得return
  3. 在洛谷上的的评测时的op用scanf就全部炸掉了;【要用cin】

code

#include<bits/stdc++.h>
using namespace std;

const int nn=30010;

int t;
int fa[nn],size[nn],d[nn];
int ans=0;

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

int gf(int x)
{
	if(x==fa[x])	return x;
	int root=gf(fa[x]);
	d[x]+=d[fa[x]];
	return fa[x]=root;
}

int main()
{
	/*初始化*/
	for(int i=1;i<=nn;++i)
	{
		fa[i]=i;
		size[i]=1;
	}
	
	t=read();
	for(int i=1;i<=t;++i)
	{
		char op;
		cin>>op;
		if(op=='M')
		{
			int x=read(),y=read();
			int xx=gf(x),yy=gf(y);
			fa[xx]=yy;
			d[xx]=size[yy];
			size[yy]+=size[xx];
		}
		else if(op=='C')
		{
			int x=read(),y=read();
			int xx=gf(x),yy=gf(y);
			if(xx==yy)	ans=abs(d[x]-d[y])-1,printf("%d\n",ans);
			else		printf("-1\n");
		}
	}
	return 0;
}

做题时的思考
Q:为什么这道题可以用并查集?
A:首先是因为这道题在题目中有并查集最关键的两个操作:合并和查询,而且这道题中只有这两个大方面的操作,所以这道题可以用并查集;
其次在并查集的学习中,确实是有带权并查集这样的一个东西,而且这道题符合带权并查集的概念,要保存子节点到父亲节点和祖先节点的距离,所以要用并查集。

小技巧:
在读入并且判断字符的时候:
1.判断的清楚一些,等于哪个,不等于哪个;
2.cin是会忽略空格和换行的;scanf和getchar是什么都会读入的,只要是一个字符;
3.最好使用cin。

做完题的体会
并查集不仅仅是可以维护两个集合所属关系,其实还可以带一些额外的有关集合的信息,就比如说这道题的两个元集合之间的距离。

并查集还是一种很神奇的算法的,做了那几道题之后就感觉到了,如果是用图来维护这些信息的话,那么必然是一个冗长的代码,而并查集的话就短短的几行代码,prefect!

parity game

题目链接:http://poj.org/problem?id=1733
做这一道题的时候有两个思路,接下来就是介绍时间啦!

1.带权并查集

首先要纠正一下自己的思维漏洞:
自己只想到了(l=[1,l]):
若l与r的奇偶性相同,那么[l,r]的奇偶性才相同;
若l与r的奇偶性不同,那么[l,r]的奇偶性也不同。

(而且这里还有一个细节,就是这里的[l,r]的奇偶性的判断应该是由[1,l-1]和[1,r]的奇偶性判断)

其实可以推广到更一般的情况呢(三个数字的比较):
若x1,x2的奇偶性相同,x2,x3的奇偶性相同,此时x1,x3的奇偶性相同;
若x1,x2的奇偶性相同,x2,x3的奇偶性不同,此时x1,x3的奇偶性不同;
若x1,x2的奇偶性不同,x2,x3的奇偶性不同,此时x1,x3的奇偶性相同。
这样子的话就不需要根节点非得是从1开始,而可以是任意的一个数字。l

其次是思路:
1.由于数据过大,所以要离散化;
2.并查集的惯用思路,fa数组的初始化;
3.程序的判断,需要分类讨论:
若这一组数据未在同一个集合,也就是说,这一组数据还没出现过,所以要先合并;
反之,出现过了之后就可以直接判定这组数据对还是不对。
所以说,还是离线做。

Q:怎么合并?
A:通过异或的一些性质
x xor y = z;
y xor z = x;
x xor z =y;
假设,fa[xx]=yy
所以,ans=d[x] xor d[y] xor d[xx];
d[xx]=ans xor d[x] xor d[y];

Q:这里为什么增加的是,d[xx],而不是d[yy]?
A:因为,如果我们把图画出来的话,就可以发现,在整个图合并之后,就仅仅只增加了xx->yy的一条边,而且xx是yy的子节点,所以应该增加的是xx的价值(增加的是儿子节点的价值)。

注意
在这道题中特指的是区间之间的奇偶性的问题。
那么你的 l 就应该变成是 l-1
原因:
若是[1,l-1]与[1,r]的奇偶性相同,那么区间[l,r]的奇偶性就相同;
若是[1,l-1]与[1,r]的奇偶性不同,那么区间[l,r]的奇偶性就不同;

code

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;

const int nn=10010;

int n,m;
int a[nn*2],fa[nn*2],d[nn*2];

struct number
{
	int x,y,ans;
}nu[nn];

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

inline void read_decrete()
{
	n=read(), m=read();
	for(int i=1;i<=m;++i)
	{
		char str[5];
		nu[i].x=read(),nu[i].y=read(); scanf("%s",str);
		if(str[0]=='o')	nu[i].ans=1;/*奇数*/
		else			nu[i].ans=0;/*偶数*/
		a[i*2-1]=nu[i].x-1;
		a[i*2]=nu[i].y;
	}
	sort(a+1,a+1+2*m);
	n=unique(a+1,a+1+2*m)-(a+1);
}

int gf(int x)
{
	if(x==fa[x])	return x;
	int root=gf(fa[x]);
	d[x]^=d[fa[x]];
	return fa[x]=root;
}

int find(int x)
{
	return lower_bound(a+1,a+1+n,x)-a;
}

int main()
{
	read_decrete();
	for(int i=1;i<=n;++i)	fa[i]=i;
	for(int i=1;i<=m;++i)
	{
		int x=find(nu[i].x-1),y=find(nu[i].y);
		int xx=gf(x);
		int yy=gf(y);
		if(xx==yy&&d[x]^d[y]!=nu[i].ans)
		{
			printf("%d\n",i-1);
			return 0;
		}
		else
		{
			fa[xx]=yy;
			d[xx]=nu[i].ans^d[x]^d[y];
		}
	}
	printf("%d\n",m);
	return 0;
}

2.扩展域并查集

对于这道题而言还有一种解决方法是扩展域并查集。

Q:为什么可以使扩展域并查集?
A:因为这道题都可以把每一个数字分成两个区域(奇数域和偶数域),那么就可以根据情况把每个数字不同的区域合并,从而可以根据查找两个数字不同的域的所属关系,判断条件是否成立。

思路:
1.同样是离散化;
2.并查集的初始化(不过这个的并查集是2*n)【因为有两个不同的域】
3.根据条件判断,若条件不符,直接跳出;否则再赋值。

Q:为什么要再次赋值?
A:可以不用再次赋值如果他们提前就相等了的话,但是这样的话有需要if……,else……,而且如果再次赋值的话也没有影响,为了减少代码量,所以再次赋值。

code

#include<bits/stdc++.h>
using namespace std;

const int nn=5010;

int m,n;
int fa[nn*4],a[nn*2];

struct number
{
	int x,y,ans;
}nu[nn];

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

int find(int x)
{
	return lower_bound(a+1,a+1+n,x)-a;
}

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

int main()
{
	n=read(), m=read();
	for(int i=1;i<=m;++i)
	{
		char str[6];
		nu[i].x=read(),nu[i].y=read();
		scanf("%s",str);
		if(str[0]=='o')	nu[i].ans=1;/*奇数*/
		else			nu[i].ans=0;
		a[i*2-1]=nu[i].x-1;/*注意这里是x-1*/
		a[i*2]=nu[i].y; 
	}
	sort(a+1,a+1+2*m);
	n=unique(a+1,a+1+2*m)-(a+1);
	for(int i=1;i<=2*n;++i)	fa[i]=i;
	for(int i=1;i<=m;++i)
	{
		int x=find(nu[i].x-1),y=find(nu[i].y);
		int x_odd=x, x_even=x+n;
		int y_odd=y, y_even=y+n;
		if(nu[i].ans==0)/*说明奇偶性相同*/
		{
			if(gf(x_odd)==gf(y_even))
			{
				printf("%d\n",i-1);
				return 0;
			}
			fa[gf(x_odd)]=gf(y_odd);
			fa[gf(x_even)]=gf(y_even);
		}
		else if(nu[i].ans==1)
		{
			if(gf(x_odd)==gf(y_odd))
			{
				printf("%d\n",i-1);
				return 0;
			}
			fa[gf(x_odd)]=gf(y_even);
			fa[gf(x_even)]=gf(y_odd);
		}
	}
	printf("%d\n",m);
	return 0;
}

食物链

思路:扩展域并查集和边带权并查集

第一次得到了30分。。。。垃圾死我了。。。

错因
分析问题不完全。

1.若是1 x y

那么成立的条件是:

  1. x的同族就是y的同族;
  2. x的捕食对象就是y的捕食对象;
  3. x的天敌就是y的天敌。

不成立的条件

  1. x的捕食对象是y的同族;
  2. y的捕食对象是x的同族。

2.若是 2 x y

那么成立的条件是:

  1. x的捕食就是y的同族;
  2. x的天敌就是y的捕食对象。(因为题目中说一共只有三种生物,且三种成环)
  3. y的敌人就是x的同族;

对立条件;

  1. y的捕食对象是x的同族;
  2. x的同族是y的同族。

code

#include<bits/stdc++.h>
using namespace std;

const int nn=50010;

int n,k;
int ans=0;
int fa[nn*3]; 

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

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

int main()
{
	n=read(), k=read();
	for(int i=1;i<=3*n;++i)	fa[i]=i;
	for(int i=1;i<=k;++i)
	{
		int op=read(),x=read(),y=read();
		if(x>n||y>n){ans++; continue;} 
		int x_eat=x+n, x_e=x+n+n;
		int y_eat=y+n, y_e=y+n+n;
		if(op==1)/*同类区*/
		{
			if(gf(x_eat)==gf(y)||gf(y_eat)==gf(x))
			{
				ans++;
				continue;
			}
			fa[gf(x)]=gf(y); fa[gf(x_eat)]=gf(y_eat);
			fa[gf(x_e)]=gf(y_e);
		}
		else/*x吃y*/
		{
			if(gf(x)==gf(y)||gf(x_e)==gf(y))
			{
				ans++;
				continue;
			}
			fa[gf(x_eat)]=gf(y);
			fa[gf(y_e)]=gf(x); 
			fa[gf(x_e)]=gf(y_eat);
		}
	}
	printf("%d\n",ans);
	return 0;
}

特殊案例

还有一类例子说的是并查集的分离,当然这个太过于高深的算法,我不会。。

例题:猴子

Q:为什么这道题可以用并查集做?
A:
1.因为这道题可以看成是若干个连通块的问题,如果有点始终存在于1号的连通块内,那么就始终不会掉落,那么其他的会掉落的连通块就是看根节点的最早的掉落时间即是整个连通块的掉落时间。若干个连通块的问题的连接和合并(或者是断开)就可以用并查集问题。
2.但是此时并查集的断开需要更加高深的知识(我不会),所以我们就可以倒序处理这m条关系,换成是加边。

  1. 末状态倒推初状态,判断最后连接到第一只猴子的时间就是这一坨猴子掉下的时间。
  2. 对于各个点都用并查集维护,它掉落的时间由他的所有的祖先中松手最早的那一只猴子决定

Q:为什么最后要倒序处理?
A:因为正序的搞的并查集的断开,很难,所谓我们此时应该换一个思路。我们倒序,变成是并查集的加边,把断开的时间转化成是连上第一个节点的时间。

code

#include<bits/stdc++.h>
using namespace std;

const int nn=200010;
const int mm=400010;

int n,m;
int fa[nn],ans[nn];
int son[nn][3];
bool d[3][nn];

struct contact
{
	int x,y;
}c[mm];

inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0'; ch=getchar();}
	return x*f;
}

int gf(int x)
{
	if(x==fa[x])	return x;
	int root=gf(fa[x]);
	ans[x]=min(ans[x],ans[fa[x]]);
	return fa[x]=root;
}

void merge(int x,int y,int z)
{
	int xx=gf(x), yy=gf(y);
	if(xx!=yy)
	{
		if(xx==1)	{fa[yy]=xx; ans[yy]=z;}
		else		{fa[xx]=yy; ans[xx]=z;}
	}
}

int main()
{
	n=read(), m=read();
	for(int i=1;i<=n;++i)
	{
		fa[i]=i; ans[i]=1e9;
		son[i][1]=read(), son[i][2]=read();
	}
	for(int i=1;i<=m;++i)
	{
		c[i].x=read(); c[i].y=read();
		d[c[i].y][c[i].x]=true;/*标记一下*/
	}
	
	for(int i=1;i<=n;++i)
	{
		if(!d[1][i]&&son[i][1]!=-1)
			merge(i,son[i][1],1e9);
		if(!d[2][i]&&son[i][2]!=-1)
			merge(i,son[i][2],1e9);
	}
	
	for(int i=m;i>=1;--i)
		if(d[c[i].y][c[i].x]&&son[c[i].x][c[i].y]!=-1)
			merge(c[i].x, son[c[i].x][c[i].y], i-1);
	printf("-1\n");
	for(int i=2;i<=n;++i)
	{
		gf(i);
		if(ans[i]==1e9)	printf("-1\n");
		else			printf("%d\n",ans[i]);
	}
	return 0;
}

巩固练习

之后可以做做习题,关押罪犯,还可以自己再找一些题,做一下。

推荐关押罪犯的博客:https://blog.csdn.net/qq_43093454/article/details/94389017


如果只是友情的话,能好好做朋友的就好好做朋友吧。不要太贪心了,爱情这种事太极端,要么一生,要么陌生。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值