有人说:并查集好写,好用,就是没什么地方用。的确,并查集应用不是特别广,但作为一种优质算法,这里还是要多说几句。并查集的问题主要分成两大类:带权并查集,种类并查集。
一、带权并查集
带权并查集就是让每个节点除了记录自己父亲以外,还记录一些其它的东西(如:集合的大小),通过它记录的信息来解决题目。
例题1:(来源: caioj 1095)
1. M i j :合并指令,i和 j是指令涉及的战舰编号。该指令是将i号战舰所在的整个战舰接至第 j号战舰所在的战舰队列的尾部。
2. C i j :询问指令,i和 j是指令涉及的战舰编号。第 i 号战舰与第 j 号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
看到“合并”,很容易想到并查集,这题合并的战舰要求保持有序,那对并查集的要求有点高了。
思路:我们可以对战舰组进行编号,战舰头记为1,战舰尾在舰队中的编号特别记录在tail中,由战舰头负责管理。在战舰组中的每艘战舰要记住该战舰组的头,它就像是舰队的首领,同时,不能忘记自己在战舰组中的位置。这样,对于每次询问,我们只要先看看这两艘战舰是否在同一战舰组,是,则输出它们的位置差。
代码:
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int fa[30010],dis[30010],tail[30010];//dis记录战舰在舰队中的位置 tail记录该战舰的尾战舰在舰队中的编号,也能反映舰队的大小
char c[5];
int find_fa(int x)
{
if(fa[x]==x) return fa[x];
int f=find_fa(fa[x]);//先让舰队头更新当前时局
dis[x]=dis[x]+dis[fa[x]]-1;//自己再更新
fa[x]=f;
return fa[x];
}
int main()
{
int T;
scanf("%d",&T);
for(int i=1;i<=30000;i++)
{
fa[i]=i;
dis[i]=1;
tail[i]=1;
}
while(T--)
{
int x,y;
scanf("%s%d%d",c,&x,&y);
if(c[0]=='M')
{
int fx=find_fa(x),fy=find_fa(y);
fa[fx]=fy;
dis[fx]=tail[fy]+1;//fx更新自己在舰队中的位置
tail[fy]=tail[fy]+tail[fx];//fy更新扩展后的舰队的信息
}
else
{
int fx=find_fa(x),fy=find_fa(y);
if(fx!=fy)
{
printf("-1\n");
continue;
}
printf("%d\n",abs(dis[x]-dis[y])-1);
}
}
return 0;
}
二、种类并查集
种类并查集就是已经给出了两两关系判断方式的并查集,要求你根据这种判断方式来求出其中某两个的关系。通常我们需要用一个re数组,记录下我与我父亲的关系,此外还要求出关系的转移式,使得每个集合内的关系和合并集合时的关系能够准确无误的展现。
例题2:(来源: caioj 1096)
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
第一种说法是“1 X Y”,表示X和Y是同类。
第二种说法是“2 X Y”,表示X吃Y。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。当前的话与前面的某些真的话冲突,就是假话;当前的话中X或Y比N大,就是假话;当前的话表示X吃X,就是假话。 输出假话的总数。
如果用一个belong数组简单记录下每种动物的属性,在合并两条食物链时,会显得无从下手,既然又栽在了合并的问题上,为什么不考虑并查集呢?
思路:用re数组记录下我与父亲的关系:0:我与父亲同类,或者我没有父亲;1:我吃父亲;2:我被父亲吃。
如图(箭头从吃指向被吃),我们来理一理关系的计算法则。
D与A的关系应该为0,其实是(re[x]+re[fa[x]])%3的结果。由此,我们得到,一条食物链上的关系可以用加法解决。
A与C的关系应该为1(即A吃C),其实是(3-re[x])%3的结果。由此,我们得到,若将关系的发出者交换,关系为(3-re[x])%3。
B与C的关系应该为2(即B被C吃),根据结论2,我们可以把B与A的关系和C与A的关系,转换为B与A的关系和A与C的关系,现在B和C就在一条顺着的关系上了。再根据结论1,我们得到B与C的关系为(3-re[y])%3+re[x],整理得(re[x]-re[y]+3)%3。由此,我们求得了与祖先有直接关系的动物跨过祖先后,它们关系为(re[x]-re[y]+3)%3。
接下来,如果发现X与Y在一条食物链里,我们让X与Y直接与父亲相连(路径压缩),那么我们通过 (re[x]-re[y]+3)%3 可以找到X与Y的关系;特别的,当X或Y没有父亲时,也不影响通过该公式求出两个动物间的关系。
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,k,ans=0;
int fa[50010],re[50010];//x与fa[x]的关系(0,1,2)
int find_fa(int x)
{
if(fa[x]==x) return fa[x];
int f=find_fa(fa[x]);
re[x]=(re[x]+re[fa[x]])%3;//关系公式
fa[x]=f;
return fa[x];
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
fa[i]=i;
re[i]=0;
}
while(k--)
{
int d,x,y;
scanf("%d%d%d",&d,&x,&y);
if(x>n||y>n) ans++;
else if(d==2&&x==y) ans++;
else
{
int fx=find_fa(x),fy=find_fa(y);
if(fx==fy)//在一条记录好了的食物链中
{
if((re[x]-re[y]+3)%3!=d-1) ans++;//关系公式
}
else//以前没有记录过关系,此话为真,所以记录下它们的关系
{
fa[fx]=fy;
if(d==1) re[fx]=(re[y]-re[x]+3)%3;//关系公式
else re[fx]=(1-re[x]+re[y]+3)%3;//关系公式
}
}
}
printf("%d",ans);
return 0;
}
总结:对于种类并查集,先制定好种类编号,用加法的形式检验。合格后,继续寻找其它的关系公式。最后,应用到并查集模版中。
通过以上两题,有没有发现,对于复杂些的并查集,它的find_fa函数总是有以下格式:
int find_fa(int x)
{
if(fa[x]==x) return fa[x];
int f=find_fa(fa[x]);
//随意干些奇奇怪怪的事
fa[x]=f;
return fa[x];
}
要记得先让旧父亲寻找新父亲,接着处理自己的事,最后才更新自己的最新父亲。因为要使旧父亲的情况与最新情况同步,自己才能把与新父亲的关系处理好,进而抛弃旧父亲,认最新的父亲。
推荐:《并查集—入门》http://blog.csdn.net/a_bright_ch/article/details/77161640