讲课三连
由于水平有限,并不能将并查集讲的透透彻彻,只是自己的一点见解,在做题目中看到的, 自己整理一下。个人觉得并查集有很多种,比如普通并查集,带权并查集,种类并查集,目前我只是见到过这三种(由于水平有限)。今天只是说说最普通不过的并查集。
先推荐个博客(风趣的讲解,但我感觉我还是没有理解并查集的本质)(还是我太菜了)。 武侠版并查集 写的不错,大家可以看看,试着理解一下。
接下来我该胡扯扯了,这个并查集,由名字而来就是合并(join)和查(find)功能集合在一起的一个算法吧。以我的理解就是将一堆东西根据某种关系集合在一起,关系为吃或被吃,是否为伙伴或者为敌人等等多种关系。并查集的题型种类也有很多种,什么推理逻辑,每个人说法之间互相矛盾,什么食物链 ,什么道路相互交通,等等一堆问题(如果没有学过的话,会一头雾水,不知从哪做起).我感觉并查集主要还是 一中思想,具体的实现(用模板)还是比较好实现的。以下是我瞎扯的,如果有出入,一定要评论留言,希望不吝赐教。
我先说说并查集的最普通的东西吧,这个并查集就相当于一个树的结构, 并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询的问题。常常在使用中以森林来表示。集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。。
在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这样的问题看起来似乎很简单,每次直接暴力查找即可,但是我们需要注意的问题。是,在数据量非常大的情况下,那么时间复杂度将达到O(N*n)(n为查询次数),那么这类问题在实际应用中,如果采取上述方法去做的话,耗费的时间将是巨大的。而如果用常规的数据结构去解决该类问题的话(顺序结构,普通树结构等),那么计算机在空间上也无法承受。所以,并查集这种数据结构便应运而生了。这个解释就写的 很好 ,就是那种逻辑问题中怎样处理集合之间关系,和在查询时,怎么快速查到彼此之间的关系。并查集主要是在一个大集合里面找出一个最顶的代表节点,这个节点就可以代表这整个集合。(大神的理解)
首先我们需要先灌输集合的思想(就像数学上的集合定义,有一类相同性质的集合)这个跟数学也有点像。比如国之间的关系,一开始我们单个国之间自己都是一个集合(与他国没有关系),然后某天三国争霸,大国吞并小国,收为小弟,开疆扩土,这就成为了大国的臣子,这样大国的集合就会新来一个小弟,串连在一起,哪天小弟受欺负,直接报出老大的姓名。而其他大国与小国也会有自己单独的集合,没有相交。在这里停一下,那你可能会有一些疑问,我们该怎么表示小弟与大国之间的关系呢?(不会有更变态的数据结构来实现吧) 不不,想多了,就直接用一个数组就可以实现了,你可能会想,不是一个大国下面可以有很多小弟吗,难道直接一个普通的一维数组就可以搞定了,为什么呢?,现在不知道很正常。我说一下,一个小弟称打败他的大国为大哥,这个就是个父节点与子节点的关系,就双方两个人,所以我们就直接用一维数组就可以实现,数组中装的是这个小弟的大哥;那到这里,我们就先说一说,怎么确定两个人的关系呢?(我们得先确定这两个集合是不在一块的,这个得用find函数,下面再说,我们现在就先假设没有在一块),一开始我们就是pre[maxn] 数组中数据都是自己单独为一个集合,现在有了老大,就把这个节点的父亲节点换成这个老大 。现在我们又有一个问题了,就是说这个老大现在在你面前是个老大,但在另一个大国面前就又是一个小弟了,所以为了更快地 找到这个集合的代表人物(佬中佬),我们就进行路径压缩,将这一串节点都间接的连在一块。这样就可以在判断是不是一个相同的集合时,就可以快速判断(在查找的时候会有数据更新,就是上级不断在更新,下面就可以知道了) ,(看题目的具体要求)我们就可以处理关系之间的问题。
在说这个 find 函数前,我还是提一嘴吧( (辣鸡就是我) 以下纯属瞎说,如有出入,请不吝赐教),一维数组实现将数据连成串,我将这个数组下标与数组数据应用起来,一开始全都是pre[i]=i , 只要pre[i] 不等于i的时候,就说明这个串还没有完,还可以往后面走,直到pre[i]==i 的时候,我感觉这个(好像串式的数组有点像链表,在很多方面都有很多用处)。
既然集合数组已经知道了,那我们下面看看find 函数,和这张路径压缩图(武侠版里面的图)。很清晰,借助理解。
将路径压缩,主要是借助这个节点数组(pre[maxn])和一个find 函数来实现的 。首先这个 pre[maxn]数组中数据是 pre[i]=i ,一开始每个集合就是一个独立的集合,没有交叉点。上面是我们说过了,就是用一个一维数组来实现这个功能。
这个find 函数的意义就是从地下的这个点往上返,知道一个节点 没有父亲节点(就是没有大哥的节点就可以作为一个代表,就是 pre[x]=x的情况,这个就可以了,不用再找了)。在找的过程中,不断更新pre[maxn]数组,尽量更新为这个代表节点(因为有的节点1 2有关系,2 3有关系)。
int find(int x)
{
if(x!=pre[x])
{
int temp=find(pre[x]);
pre[x]=temp;
}
//直接一个递归式子搞定 return x==pre[x]?x:pre[x]=find(pre[x]);
return pre[x];
}
find 函数就是这个,虽然看起来简单,但是往上返回的过程中可能会有其他操作(有的题目会有要求,用这个函数来实现,等到下面再看吧)。
接下来看看这个join函数,这个函数就是来判断两点之间关系(就是在这个函数里面调用,看看有没有连在一块),如果没有在一块的话,看题目中的要求分配关系(有的题目会有很多的关系比如 是一伙的,不是一伙的,关系不确认,或者更多的关系),这样就可以处理很多关系的询问。**** 还有重要的一点就是这个join 函数里面的将两者关系并在一起是要将两者的上级最终节点并在一起,不是简单的将这个节点与另一个单节点并在一起,这个下标肯定是不能动的,所以我们可以用来移动这个上级值来串更多的节点(就像那个模拟链表一样串起来,所以改变的是上级,不是这个下标,注意***),上面说过,集合里面的元素都是有一类相同的性质的,比如城与城之间的道路,只要连在一起了,就可以全都走;一个团体中新加入了一个人,这集合里面原先所有的人与这个新人的关系都是一样的平等的,有的题目有很强的这个特征(可以来判断能不能用并查集来做)。
void join(int x,int y)
{
int a=find(x);
int b=find(y);
if(a!=b)
{
//这个里面看题目的要求进行
//更多的操作
pre[a]=b; //看题目的要求,看看是谁并谁
}
}
由于我的水平有限,上面就是并查集的一些基本模板,但是这些还是远远不够的 ,其中的一些操作真的是很难想(我有时也会想错),下面我将说说我对各种题型的总结(希望对大家有用,并且我自己也总结一下这个并查集)。
1.普通并查集
畅通工程
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
Input
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
Output
对每个测试用例,在1行里输出最少还需要建设的道路数目。
Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0
Sample Output
1
0
2
998
#include<iostream>
#include<cstdio>
#include<queue>
#include<algorithm>
#include<bits/stdc++.h>
#include<set>
#include<vector>
const int maxn=10005;
using namespace std;
#include<map>
#include<string>
#include<string.h>
typedef long long ll;
typedef pair<int,int> p;
int pre[maxn];
int n,m;
void init()
{
for(int i=1;i<=n;i++)
pre[i]=i;
}
int find(int x)
{
return x==pre[x]?x:pre[x]=find(pre[x]);
}
int main()
{
while(scanf("%d",&n)!=EOF&&n&&scanf("%d",&m)!=EOF)
{
init();
int ans=n-1;
int a,b;
for(int i=0;i<m;i++)
{
scanf("%d%d",&a,&b);
int fx=find(a);
int fy=find(b);
if(fx!=fy)
{
ans--;
pre[fx]=fy;
}
}
printf("%d\n",ans);
}
return 0;
}
这个题目就是一个最普通的入门题目,求出将所有的城镇都连同需要修建最少的道路(直接相连和间接相连都是实现了相连)。我细细的分析下这个问题。这个道路肯定是有两个端点城镇,看这两个城镇是否已经连同,只有连同或者不连通两种情况,联通的话就不用管这两个城镇,如果不联通那就需要新建设一条路。
这个题目我是感觉有点难的,主要是初始条件为什么就设置为城镇数量-1 (下面我说我的想法)。
这个题目是要求出还需要修建的道路数量,我们可以想,整个城镇需要连起来要想小的话,不能出现城镇环的现象,就是这个情况,我们就多建了一条,所以说不能出现环的现象,就是一个串就可以将全部的城镇连同起来,这种情况就是最短的时候了。所以我们一开始的初始需要修建道路数量为 城镇数量 -1.(我感觉我还没有理解透彻)。 然后再开始下面的并查集路径压缩。
下面是并查集的部分:
题目给出几个城镇,和已经修建的道路,问我们还需要最少建设多少道路,我们可以想,我们可以这样想,1 2 3 4 城镇,我们先修建 1 -> 3的 ,然后在修建一个 3 -> 4 的,然后1 就可以到达3,4 两个城镇,这就减少了一步了,所以就按照这个思路,我们使用给出的已修建的道路,尽量构造出来一个串,尽量能到达其他更多的城镇,这就是上面的那个 find 和 join 函数,能合并的路线,就先合并,这样在这个串中任意两个城镇之间都可以间接到达,就不用再修路了(这样就可以避免了道路环的出现,这样就可以是道路数量最小了)。到达这里可能看上面的代码,还是一头的雾水。(我感觉这个并查集部分与上面那个 修建道路初始值应该分开想,就是两码事,这个并查集判断两个集合间是不是已经连上了,是不是两个集合)。
下面就是代码部分:
题目先给出已经修建好的之间道路,代码的意思是从需要(当前还没有修,是最多)修建的路中减去已经修好的道路(想了半天哎),是在输入过程中判断两个点在不在一个集合中,如果不在,那就将需要的道路减1,如果在一个集合里面的话,就不用减,遍历完所有输入数据剩下的需要修建的道路就是最小的答案,有点巧妙,很有思想(我感觉,可能是我太辣鸡了),希望对你理解有帮助。
普通并查集还有一些其他题目,poj 2524 hdu1213
2.带权并查集
有的并查集可能会有权值(好像一会我说的好像不是带权值,好像只是一个并查集的应用),就是比如一个节点有传染性,然后与他一个集合中的都会被传染,然后最后求出被传染的人数,这个就需要另一个数组来记录这个数量。那这个是怎么来操作的呢?我们需要一个数组 sum[ ]来记录每个集合中的人数,如果是一个队的关系,那就选择一个人作为基准点,都串起来然后权值也会由这个点更新数据。让我们看看这个更新数据是怎么来完成得?比如1 2 3 4 5 ,5个人,2 3 4 5 是一队的,我们需要将 2 3 4 5 都并在一起,我们选择2作为一个小队长(不是一个集合的最高代表人)吧,然后将后面的节点都从这个小队长开始串起来;这里也可以各个小队员串到这个小队长这里(这种情况的话,这个小队长就是这个集合的最高代表了),具体这个权值在下面这个join函数中详细解说。不管是哪个,这个权值数组都要按相对的关系来,意思就是你串起来的方向不一样,这队权值和加到哪个节点是不一样的。一样的话,那就错了。
下面就是找出一个小队长的代码:
cin>>k>>num1; //k是队员数量,num1是选的小队长
for(int j=1;j<k;j++)
{
cin>>num; //每个小队员
join(num1,num); //并起来
}
还有这个join 函数中,会有一点小小的改变:
void join(int a,int b)
{
//a是小队长
//b是小队员
int fa=find(a);
int fb=find(b);
//以下两种写法是一样的,最后的总权值都是在最高代表那里
if(fa!=fb)
{
pre[fb]=fa; //这种这个小队长为本集合最高代表
sum[fa]+=sum[fb]; //权值都加在这个小队长这里,最高代表都是这个小队长,不变
/*
pre[fa]=fb; //这种是从小队长出发,一直走到最高代表,然后将fb 设置为最高代表
sum[fb]+=sum[fa]; //权值都加在最后那个最高代表那里,这个最高代表是一直动的,权值也是
//权值也是在移动中累加的,因为有find函数
*/
}
}
还是一开始说的,移动的是上级,不是下标。对于两个不同的大集合,也是这样的运作机制,将两个集合中的最高代表合并一下,并且权值加到那个最高代表那里就可以了。
最后如果要知道某个节点所在集合权值的话,就直接调用一步 find 函数,就可以了,直接找到他的最高代表作为sum[ ]数组的下标输出人数就可以了(因为集合总权值)。代码:
int q=find(0); //这个0 是任何要查的节点
cout<<sum[q]<<endl;//输出权值
The Suspects
一起学猫叫病毒( Miaomiaomiao Together), 是一种原因不明的流行性病毒。从去年起,一起学猫成为新一代校园毒品,听到一起学猫叫这首歌的人很有可能就会感染这种病毒。由于它传染性很强( 只要你周围有人唱一起学猫叫,你很有可能也会开始唱,并开始劝你的同学一起唱)它开始被认为是全球威胁。为了减少传播给别人的机会, 最好的策略是隔离可能的患者。
学校里有许多学生团体。同一组的学生经常彼此相通,一个学生可以同时加入几个小组。为了防止一起学猫叫病毒病毒的传播,cjluxk算法与程序设计协会收集了所有学生团体的成员名单。他们的标准操作程序如下:
一旦一组中有一个可能的患者, 组内的所有成员就都是可能的患者。
为了遏制这种病毒的传播,我们需要找到所有的患者。现在知道编号为0的如花妹妹(感染源)已经得了一起学猫叫病毒,请你设计程序 发现所有可能的患者。
Input
输入文件包含多组数据。
对于每组测试数据:
第一行为两个整数n和m, 其中n是学生的数量, m是团体的数量。0 < n <= 30000,0 <= m <= 500。
每个学生编号是一个0到n-1之间的整数,一开始只有0号的如花妹妹被视为患者。
紧随其后的是团体的成员列表,每组一行。
每一行有一个整数k,代表成员数量。之后,有k个整数代表这个群体的学生。一行中的所有整数由至少一个空格隔开。
n = m = 0表示输入结束,不需要处理。
Output
对于每组测试数据, 输出一行可能的患者。
Sample Input
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Sample Output
4
1
1
这个题目就是这样的题目,我就不说了,直接放代码了:
#include<iostream>
using namespace std;
const int maxn=30005;
int pre[maxn];
int sum[maxn];
int n,m;
int find(int x)
{
return x==pre[x]?x:pre[x]=find(pre[x]);
}
void join(int a,int b)
{
int fa=find(a);
int fb=find(b);
if(fa!=fb)
{
pre[fa]=fb;
sum[fb]+=sum[fa];
}
}
void init()
{
for(int i=0;i<n;i++)
{
pre[i]=i;
sum[i]=1;
}
}
int main()
{
while(cin>>n>>m&&(n+m))
{
init();
int k;
int num1,num;
for(int i=0;i<m;i++)
{
cin>>k>>num1;
for(int j=1;j<k;j++)
{
cin>>num;
join(num1,num);
}
}
int q=find(0);
cout<<sum[q]<<endl;
}
return 0;
}
还有一类题目是给的不是整数数字,而是一个一个字符串名字,我们就可以用 map将字符串转化为 整形数字,就可以按照上面的做了其他步骤都是一样的,就是下面这个题目。
Virtual Friends
上面这种类型主要是求出由某个点找出这个点在的集合中所有的数量,比如1 2 3 4 5 在一个集合中,数量为5,查1 2 3 结果都是一样的。而下面这种类型题就不一样了。看题(嘿嘿)
带权并查集这里还有很多题型,就是在这个上级之间绕来绕去的,很容易就晕了,下面我说另一个带权的并查集,我尽量说的详细一点(我有的也不是很会,请见谅)。有了上面那道题目的基础,下面这道题有些不一样。 这道题目有了另一个需求。
下面这道题目就是真正的带权了,就是每个节点与其他节点之间都会有一定的数值关系,可以利用这些数值关系来判断他们之间的关系,就比如一些谁比谁多,谁比谁少,谁吃谁关系,都可以带上权值。
https://blog.csdn.net/yjr3426619/article/details/82315133 这个博客带权并查集讲得很好,就是里面所说的,在路径压缩过程中,我们也要更新各个节点的权值使得权值为这个点到根节点之间的权值,因为我们在后面可能会询问某个节点到根节点之间的某种关系,所以我们要精确到每个节点到根节点之间的关系。不能像上面一样在join 函数中进行操作了。是精确到每个节点的。这个博客里面有几个例题也是很好的例子,大家可以看看。
***下面是一个上面博客里面的插图,我感觉这里有一点不好,其实在合并x 和 y的时候,就没有在x和 y之间连着线(下面这个有点不好,容易误导,但是由其他的用意),这样可以得出由 px 到 py 之间的距离 是 -valueX+s+valueY ,这样更清晰一点。
现在是已知x所在的并查集根节点为px,y所在的并查集根节点为py,如果有了x、y之间的关系,要将px并到py上,如果不考虑权值直接修改parent就行了,但是现在是带权并查集,必须得求出px与py这条边的权值是多少,很显然x到py两条路径的权值之和应该相同,就不难得出上面代码所表达的更新式。但是需要注意并不是每个问题都是这样更新的,有时候可能会做取模之类的操作,这一点在后边的例题中可以体现。
下面我给出我的想法:
应该是这样子的,那么我们怎么看两点之间的权值差呢?这个权值是这个节点到根节点的距离,我们就可以看x和y到这个根节点的距离,哪个远哪个近,以这个根节点为参照系,我们就可以知道他们之间的权值差。在这里再说一点,到了最后,将每个点都进行路径压缩之后,这个图将会变成一个大根,一堆子节点,他们的权值都已经更新过,都是他们自己到根节点的权值。然后这样每两个节点之间的权值关系都可以求了。
这种类型题目 ,就是find 函数里面多了两行代码:通过递归回溯将父亲节点的权值更新到这个上面,然后进行路径压缩,这样就可以将每一个节点与根节点的权值计算出来,保存到一个数组中。带权并查集的处理方法。
int find(int x)
{
if(x!=pre[x])
{
int temp=find(pre[x]);
sum[x]+=sum[pre[x]];
pre[x]=temp;
}
return pre[x];
}
在join 函数里面也多了几行代码:在这里借助关系来计算权值大小,那么我们为什么写 value[px] = -value[x] + value[y] + s ,加到父亲节点上呢,就是上面说的,我们在最后 的时候会进行路径压缩,回溯递归计算每个节点的权值,我们我们将权值关系给到本集合最高的那个节点(最后find 进行回溯计算权值)。
int px = find(x);
int py = find(y);
if (px != py)
{
parent[px] = py;
value[px] = -value[x] + value[y] + s;
}
Dragon Balls
Five hundred years later, the number of dragon balls will increase unexpectedly, so it's too difficult for Monkey King(WuKong) to gather all of the dragon balls together.
His country has N cities and there are exactly N dragon balls in the world. At first, for the ith dragon ball, the sacred dragon will puts it in the ith city. Through long years, some cities' dragon ball(s) would be transported to other cities. To save physical strength WuKong plans to take Flying Nimbus Cloud, a magical flying cloud to gather dragon balls.
Every time WuKong will collect the information of one dragon ball, he will ask you the information of that ball. You must tell him which city the ball is located and how many dragon balls are there in that city, you also need to tell him how many times the ball has been transported so far.
有标号为1到n的n个龙珠,分别放在对应标号为1到n的n个城市里。
下面有两种操作:
T A B表示把A龙珠所在城市的所有龙珠都转移到B龙珠所在的城市中
Q A 表示查询A,需要知道A龙珠现在所在的城市,A所在的城市有几颗龙珠,A转移到这个城市移动了多少次,分别输出3个整数,表示上述信息。
Input
The first line of the input is a single positive integer T(0 < T <= 100).
For each case, the first line contains two integers: N and Q (2 < N <= 10000 , 2 < Q <= 10000).
Each of the following Q lines contains either a fact or a question as the follow format:
T A B : All the dragon balls which are in the same city with A have been transported to the city the Bth ball in. You can assume that the two cities are different.
Q A : WuKong want to know X (the id of the city Ath ball is in), Y (the count of balls in Xth city) and Z (the tranporting times of the Ath ball). (1 <= A, B <= N)
第一行一个整数T表示测试数据组数T∈[0,100]
对于每一组测试数据,第一行包含两个整数N,Q∈[2,10000]
接下来Q行,每行包含如下的操作或询问:T A B或Q A,如题目描述所示。
Output
For each test case, output the test case number formated as sample output. Then for each query, output a line with three integers X Y Z saparated by a blank space.
对于每一组测试数据,先输出一行Case x:(x表示测试数据标号,从1开始),然后对于每一个询问,输出一行三个数表示答案,用空格隔开
Sample Input
2
3 3
T 1 2
T 3 2
Q 2
3 4
T 1 2
Q 1
T 1 3
Q 1
Sample Output
Case 1:
2 3 0
Case 2:
2 2 1
3 3 2
题意就是龙珠移动到最后,一共移动了几次,(题目还要求出到达的这个城市的总龙珠数量,跟上面的一样,就不多说了)。这个转移了几次我说一说,(话说回来还是我菜的原因)。以下是我个人的见解(可能有出入,还望大神不吝赐教),这里还得需要一个数组来记录龙珠的转移次数 。初始条件个点都是0数据。这个需求是要求出每个龙珠的转移次数,如果 还想以前一样将这个数组加和关系放在join 函数中的话,他只能计算当前龙珠的转移次数,(这个有点难懂,比如说 ,将 1转移到 2处,龙珠1转移次数数组加1,那这个时候将2再转移到3里面,这里1和2是一块移动到3里面,但是你只能计算这个2移动了几次,没有办法看这个1移动数量又加了一个,但是我们这个时候没有办法计算,所以这种方法不适用于计算这种中间转移过程变量和每一个节点的查询,我们需要另寻办法)。既然后面的关系会影响到前面的权值,然后我们就可以使用递归回溯将这个权值加回来,就跟上面的一样 ,于是就将这个函数放在了find 函数里面来回溯,下面我详细说一下这个函数具体运作机制:
int find(int x)
{
if(x!=pre[x])
{
int temp=find(pre[x]); //递归
tran[x]+=tran[pre[x]]; //根据上下级加到将转移次数 从pre[x] 加到 x上
pre[x]=temp;
}
return pre[x];
}
void join(int x,int y)
{
int a=find(x);
int b=find(y);
if(a!=b)
{
pre[a]=b;
sum[b]+=sum[a]; //与这个转移次数无关,先不用看
sum[a]=0; //也先不用看
tran[a]=1; //重点
}
}
可能会想,怎么处理才能这个数量加到该加的地方呢?(为什么加到该加的地方呢?因为询问的是每个节点,不是一类的节点,可能每个节点的转移次数都是不一样的,所以我们输出这个转移次数的时候,我们就按照这个节点就输出就可以了,但是我们还需要将数据加到这个 本身的节点上)上面已经说过了,我们一般都是使用数组下标和这个上级来连成串,我们就可以使用这个串,将值从后面加到前面来,这样想的话,我们每次join 的时候,将本集合中最高代表的那个节点权值置为1,这样我们就可以回溯的时候根据上下级关系,加到龙珠本身这个节点上,这样就可以了。 这里还要注意一点,当两个点合并的时候,不管这两个点的集合有多么的大,我们只管这两个点(可能会一头雾水,一会下面就会清楚了),比如1,2 已经连在一起了,将2连到3上,这时只将2上级 移动数组(上面建的数组)为1,这个1(移动数组)数据还是原来的1,这里是不会动的,不会改变数据为2,但是1确实是移动了两次,那是因为在这个串的只能先暂时变两者之间的转移次数,其他的还需要一些其他的操作才能 得到最终的正确的答案。这个操作就是 find 一遍这个 要查询的 节点,将数据全加到这个节点上来,就直接输出就可以了。(一定最后要进行find 回溯操作)。
代码:
#include<iostream>
#include<string>
#include<string.h>
#include<set>
#include<map>
#include<stack>
using namespace std;
const int maxn=10005;
int pre[maxn];
int sum[maxn];
int tran[maxn];
int n,q;
void init()
{
for(int i=1;i<=n;i++)
{
pre[i]=i;
sum[i]=1;
tran[i]=0;
}
}
int find(int x)
{
if(x!=pre[x])
{
int temp=find(pre[x]);
tran[x]+=tran[pre[x]];
pre[x]=temp;
}
return pre[x];
}
void join(int x,int y)
{
int a=find(x);
int b=find(y);
if(a!=b)
{
pre[a]=b;
sum[b]+=sum[a];
sum[a]=0;
tran[a]=1;
}
}
int main()
{
int kase=0;
int t;
scanf("%d",&t);
while(t--)
{
printf("Case %d:\n",++kase);
scanf("%d%d",&n,&q);
init();
char c;
int a,b;
for(int i=0;i<q;i++)
{
getchar();
scanf("%c",&c);
if(c=='T')
{
scanf("%d%d",&a,&b);
join(a,b);
}
else
{
scanf("%d",&a);
int k=find(a);
printf("%d %d %d\n",k,sum[k],tran[a]);
}
}
}
return 0;
}
如有出入,请不吝赐教。 还有个练习题,也是这个类型的差不多是一样的。
Building Block
3.种类并查集(头疼)
这个种类并查集我感觉(有点蒙圈,完全是个啥,所以大家和我说的有出入的话,请不吝赐教,谢谢)。我先举一个例子,就如人与人之间的关系(我与他是亲戚,我与他不是亲戚,还有 什么关系都不是的那种关系判断,更为复杂,比如这个关系比如1 2 是敌人,2 4是敌人,那么我问1 4 是什么关系呢?这个答案是肯定的,是友军,但是这个是怎么来判断的呢?当初看到这里是一头雾水不知道怎么处理这个关系,看网上的博客也是都看不懂,我说一下我自己的见解,如有出入,请不吝赐教)。在这里我先说一下,有的并查集也是分了几个阵营,但是它的输出条件是 a b 1, e f -1,1是朋友关系,-1是敌人关系;而有的题目就这样输入1 2是不同阵营,2 4 是不同阵营。我感觉这样的输入条件就决定了具体用什么方法来写。(因为第一种的话,谁谁的关系都说的很明确了,谁谁是友军,敌军,还有敌人敌人之间有共同的朋友一个普通并查集就可以搞定,下面我会说的;第二种的话,只是告诉你谁谁是敌人,没有告诉你谁谁是朋友,这里还有两个不确定,朋友和不确定关系都不知道)。第二种输入的话,我感觉就是敌人的敌人就是朋友(毕竟只有两个阵营),这个自己想着很好想,但是由代码实现不是很简单的(我现在还不知道是怎么来运转的)。
这个思想就是先将当前的敌对关系保存起来(下面说怎么保存),然后之后如果还有其他人与这个节点有敌对关系,再返回去这个关系现在就是同一个阵营的关系,这个就是这个思想 。我现在想的是就在原来集合的基础上再来一个集合,来表示不在同一个的阵营的关系(先用来保存这个敌对关系,我们人脑也是这种思想,敌人的敌人是朋友,我们首先得需要知道他们一开始得是敌人,所以就需要来模拟这种关系)。我们就来新建立一个敌对关系集合(是相对的,下面说),这个新建集合就是在原来的基础上多扩展了一层(就是多了一种关系,在一个阵营和不在一个阵营,毕竟一层集合只能表示一种关系,我们就再来一层关系,网上代码就使用了pre[maxn*2]前半部分或者后半部分单独使用来表示同一阵营,前半部分对应后半部分或者后半部分对应前半部分 就表示不同阵营(这个就是上面说的相对性),这样更方便的找出关系,根据每个节点的父亲节点使得关系更加复杂化,多元化)。那下面我说一下这个具体的操作流程。首先我们先构建出新建的关系集合来,一共有几种关系,我们就将这个pre[maxn] 再扩展几倍,那么我们构建新关系的时候,就是下面这种写法,那我们为什么要建立两次呢(下面会具体讲到),加上n就将集合关系分开了,join到另一个集合里面了,这样多层集合就构建好了,下面就说这个怎么操作的:
join(a+n,b);
join(a,b+n);
void join(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx!=fy)
{
pre[fy]=fx;
}
}
我们将关系构建到另一个集合中,并将其父亲节点改变,这样就相当于构建了一层关系,我现在举一个例子,就是下面的这个例子D a b ,就是a与b是敌对的关系,然后就开始构建另一个并查集,我们这里将节点+N到另一个集合里面,先构建敌对关系,然后更新这个pre数组,为什么我们要更新这个pre[ ]数组,因为我们要先构建这个敌对关系,是要在后面使用的,如果没有这个父亲节点作为转移媒介,我们也不可能找回到原来的那个集合(因为敌人的敌人是朋友,我们得先记住这个1 2是敌人才能经过2 4,判断出1 4 是同一阵营的人,所以这个find 函数 和这个父亲节点是很重要的,这个就相当于一个桥梁,将敌对关系从这边移到那边,然后那边的也可以移到这边,从而可以将敌人的敌人是朋友应用到这一点上,先找到那个敌人,再由敌人找到他的敌人,这样再返回去,就可以知道敌人的敌人是朋友了)。就像下面的这个一样。
D 1 2 2+4=6 1+4=5
1--6 5--2
pre[6]=1 pre[2]=5 //相当于构建
pre[]数组 1 5 3 4 5 1 7 8
D 2 4 2+4=6 4+4=8
6--4 2--8
find(6)=1 find(2)=5
pre[4]=1 pre[8]=5 //需要找出它原来对应的敌对
//关系然后再建出新的关系,找回去
pre[]数组 1 5 3 1 5 1 7 5
下面这到例题就是这样子的,看题:
警方决定捣毁两大犯罪团伙:龙帮和蛇帮,显然一个帮派至少有一人。该城有N个罪犯,编号从1至N(N<=100000。将有M(M<=100000)次操作。
D a b 表示a、b是不同帮派
A a b 询问a、b关系
Input
多组数据。第一行是数据总数 T (1 <= T <= 20)每组数据第一行是N、M,接下来M行是操作
Output
对于每一个A操作,回答"In the same gang."或"In different gangs." 或"Not sure yet."
Sample Input
1
5 5
A 1 2
D 1 2
A 1 2
D 2 4
A 1 4
Sample Output
Not sure yet.
In different gangs.
In the same gang.
上面的解析就是那样,就直接看代码了:
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<string.h>
#include<vector>
#include<sstream>
#include<stack>
#include<string>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const int maxn=150000;
const ll inf=(1<<30);
typedef pair<int,int> p;
int n,m;
int pre[maxn*2];
void init()
{
for(int i=1;i<=2*n;i++)
pre[i]=i;
}
int find(int x)
{
return x==pre[x]?x:pre[x]=find(pre[x]);
}
void join(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx!=fy)
{
pre[fy]=fx;
}
}
int main()
{
std::ios::sync_with_stdio(false);
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
init();
char c;
int a,b;
for(int i=0;i<m;i++)
{
getchar();
scanf("%c%d%d",&c,&a,&b);
//cin>>c>>a>>b;
if(c=='D')
{
join(a+n,b);
join(a,b+n);
}
else
{
if(find(a)==find(b)||find(a+n)==find(b+n))
printf("In the same gang.\n");
else if(find(a)==find(b+n)||find(a+n)==find(b))
printf("In different gangs.\n");
else
printf("Not sure yet.\n");
}
}
}
return 0;
}
基本的题型就是这样(我太菜了,没有见过更多的题型,大家多多包涵)