并查集 - 擒贼先擒王
我们先从一个故事说起——解密犯罪团伙。
快过年了,犯罪分子们也准备为年终奖“奋斗”了,小可可的家乡出现了多起抢劫事件。由于强盗人数过于庞大,作案频繁,警方想查清楚到底有几个犯罪团伙实在是太不容易了,不过警察叔叔还有搜集到了一些线索,需要咱们帮忙分析一下:
现在有10个强盗。
1号强盗与2号强盗是同伙。
3号强盗与4号强盗是同伙。
5号强盗与2号强盗是同伙。
4号强盗与6号强盗是同伙。
2号强盗与6号强盗是同伙。
8号强盗与7号强盗是同伙。
9号强盗与7号强盗是同伙。
1号强盗与6号强盗是同伙。
2号强盗与4号强盗是同伙。
有一点需要注意:强盗同伙的同伙也是同伙。你能帮助警方查出有多少个独立的犯罪团伙吗?
要想解决这个问题,首先我们先假设10个强盗相互是不认识的,他们各自为政,每个都是首领,他们只听自己的。之后我们将通过警方提供的线索,一步步来“合并同伙”。
步骤1:我们申请一个一维数组f,我们用下标 i(i=1->10) 表示这10个强盗,用f[i]的值代表每个强盗的boss是谁。
步骤2:初始化。根据我们之前的约定,这10个强盗都各自为政,每个强盗的boss就是自己。即f[i]=i。请注意,这是很重要的一步。
步骤3:合并。开始“合并同伙”。即如果发现母亲两个强盗是同伙,则这两个强盗是同一个犯罪团伙。现在有1个问题,合并之后,谁才是这个犯罪团伙的打boss(首领)呢?
例如警方得到的第1条线索是“1号强盗与2号强盗是同伙”。那么究竟“2号强盗”是“1号强盗”的boss呢,还是“1号强盗”是“2号强盗”的boss呢?其实这无所谓,都可以。我们这里假定左边的强盗更厉害一点,我们给起个名字叫“靠左”法则。也就是说“2号强盗”的boss是“1号强盗”。因此我们把f[2]变成1:
警方得到的第2条线索“3号强盗与4号强盗是同伙”。按照“靠左”法则,将f[4]的值要变成3(f[3])。如下:
警方得到的第3条线索“5号强盗与2号强盗是同伙”。2号强盗的boss是1号,如果按照“靠左”法则,将2号归顺5号强盗,那么1号强盗可就不干了,你凭什么抢我的人。那么2号该如何选择呢?
现在我给你支个招,嘿嘿(^_^)古语云:擒贼先擒王。你直接找2号强盗的boss和1号强盗谈,让他归顺5号强盗就OK了,也就是将f[1]的值改为5。如下:
细心的同学可能会发现一个问题:我不但将f[1]的值改为5,还将f[2]的值也改为5了,其实此处将f[2]的值改为5,不是必须的,只不过是为了提高今后找到犯罪团伙的最高领导人(其实是树的祖先)的速度。这里我们是通过递归返回的时候实现的,并不会增加算法的时间复杂度,后面再解释。
警方得到的第4条线索“4号强盗与6号强盗是同伙”。现在,f[4]的值是3,f[6]的值是6。根据“靠左”法则,让6号加入3号犯罪团伙。我们需要将f[6]的值改为3。
警方得到的第5条线索“2号强盗与6号强盗是同伙”。f[2]的值是5,f[6]的值是3。根据“靠左”法则和“擒贼先擒王”原则,让6号强盗的boss“3号强盗”归顺“2号强盗”的boss“5号强盗”。因此我们需要将f[3]的值改为5.另外我们也需要将f[6]的值也改为5,此处仍然在递归返回的时候实现。
喜欢思考的同学可能发现了,为什么f[4]还是3,f[3]和f[6]都改成5了。因为在上面一步将f[4]也改成5是需要多花费时间的,这不值得。其实f[6]的值不改成5也不会影响结果,擒贼先擒王嘛!只有f[3]的值改为5就可以了。
警方得到的第6条线索“8号强盗与7号强盗是同伙”。f[8]=8,f[7]=7,按照靠左原则,把7号归顺8号。
警方得到的第7条线索“9号强盗与7号强盗是同伙”。f[9]=9,f[7]=8.按照靠左法则和擒贼先擒王原则,f[8]的值改为9。同时f[7]的值也改为9,此步骤也在递归返回时实现。
警方得到的第8条线索“1号强盗与6号强盗是同伙”。f[1]=5,f[6]=5,他们在同一犯罪团伙,因此此条线索是冗余线索。
警方得到的最后一条线索“2号强盗与4号强盗是同伙”。f[2]=5,f[4]=3,他们竟然不在同一个犯罪团伙中?貌似不对吧!通过上图我们可以发现他们在同一个团伙中。其实这是我们再处理第5条线索的时候留下的问题,你可能要晕?
仔细观察你会发现,f[4]=3,也是说4号强盗曾经归顺3号强盗,现在看看3号归顺谁了呢?我们发现f[3]=5,也就是说3号强盗归顺5号强盗,我们再看看5号强盗归顺谁了,发现f[5]=5,原来5号就是这个团伙的boss。
我们刚才模拟的过程其实就是递归的过程。从4号强盗顺藤摸瓜一直到了他所在团伙的最高领导人。在递归的过程中,我们需要将中间遇到的“小boss”(强盗)都改为团伙的最高领导人5号强盗。最终我们发现f[2]=5,f[4]=5,他们属于同一个团伙,这条是冗余线索。
好了,线索分析完毕,通过上图可以发现一共有3个犯罪团伙。通过下面这张图可以看到,如果f[i]=i,那么此人就是一个犯罪团伙的最高领导人,有多少个最高领导人其实就有多少个独立的犯罪团伙。
上面模拟的过程就是并查集的算法。并查集是通过一个一维数组来实现,其本质是维护一个森林。刚开始的时候,森林的每个点都是独立的,之后通过一些条件,逐渐将这些树合并成一棵大树。其实合并的过程就是认爹的过程。在认爹的过程中,一定要遵守“靠左”法则和“擒贼先擒王”原则。在每次判断两个结点是否已经在同一棵树中的时候(一棵树其实就是一个集合),也要注意必须求其根源,中间“小boss”是不能说明问题的,必须要找到其祖宗(树的根结点)判断两个根结点是否在同一个根结点才行。代码如下:
c++代码如下:
#include <iostream>
using namespace std;
int f[1000]={0}, n, m, k, ans=0;
void init(){
for(int i=1; i<=n; i++) f[i]=i; //这里初始化很重要
}
//这是找爹的递归函数,不停地去找爹,直到找到祖宗为止
int getf(int v){
if(f[v]==v) return v;
else{
//这里是路径压缩,每次在函数返回的时候,顺带把路上遇到的"小boss"改为
最后找到的祖宗的编号,这样可以提高今后找到祖宗的速度
f[v]=getf(f[v]);
return f[v];
}
}
void merge(int v, int u){ //这里是合并两个子集合的函数
int t1, t2;
t1=getf(v);
t2=getf(u);
if(t1!=t2)//判断连个结点是否在同一个集合中
f[t2]=t1; //靠左法则,左边变成右边的boss,即把右边的集合,
作为左边集合的子集合。经过路径压缩以后,将f[u]的根的值也赋值为v的祖先f[t1]。
}
int main(){
int i, x, y;
cin >> n >> m;
init();
for(i=1; i<=m; i++){
cin >> x >> y;
merge(x, y); //开始合并犯罪团伙
}
for(i=1; i<=n; i++) //最后扫描有多少个犯罪团伙
if(f[i]==i) ans++;
cout << ans;
}
可以输入下列数据进行验证,第一行n和m,n表示强盗的人数,m表示警方搜集到的m条线索。接下来的m行每一行有两个数a和b,表示强盗a和强盗b是同伙。
10 9
1 2
3 4
5 2
4 6
2 6
8 7
9 7
1 6
2 4
运行结果为:3
【程序1】电影节
解题思路:
并查集模板题,直接套用模板即可。
C++ code:
##include <iostream>
using namespace std;
int f[50000]={0}, n, m, k, ans=0;
void init(){
for(int i=1; i<=n; i++) f[i]=i;
}
int getf(int v){
if(f[v]==v) return v;
else{
f[v]=getf(f[v]);
return f[v];
}
}
void merge(int v, int u){
int t1, t2;
t1=getf(v);
t2=getf(u);
if(t1!=t2) f[t2]=t1;
}
int main(){
int i, x, y;
cin >> n >> m;
init();
for(i=1; i<=m; i++){
cin >> x >> y;
merge(x, y);
}
cin >> x >> y;
if(getf(x)==getf(y)) cout << "same\n";
else cout << "not sure\n";
}
【程序2】家族
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
数据输入:
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Ai和Bi具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
数据输出:
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
样例:
input.txt
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
output.txt
Yes
Yes
No
直接套模板:
C++ code:
#include<iostream>
using namespace std;
int f[50001]={0}, n, m, k;
void init(){
for(int i=1; i<=n; i++) f[i]=i;
}
int getf(int v){
if(f[v]==v) return v;
else{
f[v]=getf(f[v]);
return f[v];
}
}
void merge(int v, int u){
int t1, t2;
t1=getf(v);
t2=getf(u);
if(t1!=t2)
f[t2]=t1;
}
int main(){
int i, x, y, p;
cin >> n >> m >> p;
init();
for(i=1; i<=m; i++){
cin >> x >> y;
merge(x, y);
}
for(i=1; i<=p; i++){
cin >> x >> y;
if(getf(x)==getf(y)) cout << "Yes\n";
else cout << "No\n";
}
}