并查集及应用
在信息学竞赛中,并查集是一种不可忽视的一部分内容,把最近几年的NOI和NOIP复赛题目大致浏览了一遍,发现有好几道应用并查集的题目,因此本文由浅入深的介绍并查集在编程中的巧妙应用。
什么是并查集?并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。集就是让每个元素构成一个单元素的集合,并就是按一定顺序将属于同一组的元素所在的集合合并。
并查集的主要操作:
1、初始化:把每个点所在集合初始化为其自身;
2、查找:查找元素所在的集合即根节点;
3、合并:将两个元素所在的集合合并为一个集合,合并两个不相交集合判断两个元素是否属于同一集合。
并查集的时间复杂度
并查集进行n次查找的时间复杂度是O(n )(执行n-1次合并和m≥n次查找)。其中 是一个增长极其缓慢的函数,它是阿克曼函数(Ackermann Function)的某个反函数。它可以看作是小于5的。所以可以认为并查集的时间复杂度几乎是线性的。
三、例题分析:
例题1、亲戚
【问题描述】
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定: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个询问的答案为“具有”或“不具有”亲戚关系。
【样例输入】
9 7 3
2 4
5 7
1 3
8 9
1 2
5 6
2 3
2 5
2 4
3 8
【样例输出】
No
Yes
No
【问题分析】
一、并查集的简单处理。
我们把一个连通块看作一个集合,问题就转化为判断两个元素是否属于同一个集合。
假设一开始每个元素各自属于自己的一个集合,每次往图中加一条边a-b,就相当于合并了两个元素所在集合A和B,因为集合A中的元素用过边a-b可以到达集合B中的任意元素,反之亦然。
当然如果a和b本来就已经属于同一个集合了,那么a-b这条边就可以不用加了。
(1)具体操作:
① 由此用某个元素所在树的根结点表示该元素所在的集合;
② 判断两个元素时候属于同一个集合的时候,只需要判断他们所在树的根结点是否一样即可;
③ 也就是说,当我们合并两个集合的时候,只需要在两个根结点之间连边即可。
(2)元素的合并图示:
(3)判断元素是否属于同一集合:
用father[i]表示元素i的父亲结点,如刚才那个图所示:
faher[1]:=1;faher[2]:=1;faher[3]:=1;faher[4]:=5;faher[5]:=3
至此,我们用上述的算法已经解决了空间的问题,我们不再需要一个n2的空间来记录整张图的构造,只需要用一个记录数组记录每个结点属于的集合就可以了。
但是仔细思考不难发现,每次询问两个元素是否属于同一个集合我们最多还是需要O(n)的判断!
3. 算法3,并查集的路径压缩。
算法2的做法是指就是将元素的父亲结点指来指去的在指,当这课树是链的时候,可见判断两个元素是否属于同一集合需要O(n)的时间,于是路径压缩产生了作用。
路径压缩实际上是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲指针都指向根结点。
这就是说,我们在“合并5和3”的时候,不是简单地将5的父亲指向3,而是直接指向根节点1,由此我们得到了一个复杂度只是O(1)的算法。
〖程序清单〗
(1)初始化:
for i:=1 to n do father[i]:=i;
因为每个元素属于单独的一个集合,所以每个元素以自己作为根结点。
(2)寻找根结点编号并压缩路径:
function getfather(v : integer) : integer;
begin
if father[v]=v then exit(v);
father[v]:=getfather(father[v]);
getfather:=father[v];
end;
(3)合并两个集合:
proceudre merge(x, y : integer);
begin
x:=getfather(x);
y:=getfather(y);
father[x]:=y;
end;
(4)判断元素是否属于同一集合:
function judge(x, y : integer) : boolean;
begin
x:=getfaher(x);
y:=getfather(y);
if x=y then exit(true)
else exit(false);
end;
faher[1]:=1;faher[2]:=1;faher[3]:=1;faher[4]:=5;faher[5]:=3
至此,我们用上述的算法已经解决了空间的问题,我们不再需要一个n2的空间来记录整张图的构造,只需要用一个记录数组记录每个结点属于的集合就可以了。
但是仔细思考不难发现,每次询问两个元素是否属于同一个集合我们最多还是需要O(n)的判断!
参考程序:
var father:array[1..5000]of integer;
i,j,k,p,n,m:integer;
Function getfather(v:integer):integer;
begin
if father[v]=v then exit(v);
father[v]:=getfather(father[v]);
getfather:=father[v];
end;
Procedure merge(x,y:integer);
begin
x:=getfather(x);
y:=getfather(y);
father[x]:=y;
end;
Function judge(x,y:integer):boolean;
begin
x:=getfather(x);
y:=getfather(y);
If x=y then exit(true) else exit(false);
end;
begin
readln(n,m,p);
for i:=1 to n do father[i]:=i;
for i:=1 to m do
begin
read(j,k);
merge(j,k);
end;
for i:=1 to p do
begin
read(j,k);
if judge(j,k) then writeln('Yes') else writeln('No');
end;
end.
例2:mty的考验(RQNOJ343)
啊!几经周折.mty终于找到了他的偶像.他就是....fyc!
可是fyc这样的高级人士可不喜欢一个人总是缠着他.于是他出了一道难题想考考mty.fyc有几个手下:陈乐天,舒步鸡,胡巍......现在fyc要去和别人fight,需要组建一值军队.军队的士兵在fyc的手下里选.
要组建一个军队,必修满足军队中的每个人之间都有直接或间接的朋友关系.
那么mty现在需要组建一支在满足上述情况下的人数最多的军队.
1<=n<=1000,1<=m<=500.
【输入格式】第一行,两个数,n,m.(n表示fyc有几个手下m表示有m对朋友关系).一下m行,每行两个数.x[i],y[i].表示编号为x[i],y[i]的人是朋友.
【输出格式】一个数,表示人数最多的军队的人数.
【样例输入】
5 3
1 2
2 3
3 4
【样例输出】
4
说明:1,2,3,4可组成一直军队.
【分析】并查集+统计
【源程序】
Program mty;
Var
tree,f:array[1..1000]of integer;
n,m,i,j,k,ans:integer;
function top(x:integer):integer;
begin
if tree[x]<>x then
tree[x]:=top(tree[x]);
top:=tree[x];
end;
Begin
readln(n,m);
for i:=1 to n do
tree[i]:=i;
for i:=1 to m do
begin
readln(j,k);
tree[top(j)]:=top(k);
end;
for i:=1 to n do
inc(f[top(i)]);
for i:=1 to n do
if ans writeln(ans); End. 例题3、关押罪犯(noip2010) 【问题描述】 S 城现有两座监狱,一共关押着N 名罪犯,编号分别为1~N。他们之间的关系自然也极 不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨 气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之 间的积怨越多。如果两名怨气值为c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并 造成影响力为c 的冲突事件。 每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表, 然后上报到S 城Z 市长那里。公务繁忙的Z 市长只会去看列表中的第一个事件的影响力, 如果影响很坏,他就会考虑撤换警察局长。 在详细考察了N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在 两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只 要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。那 么,应如何分配罪犯,才能使Z 市长看到的那个冲突事件的影响力最小?这个最小值是多 少? 【输入】 输入文件名为prison.in。输入文件的每行中两个数之间用一个空格隔开。第一行为两个正整数N 和M,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的M 行每行为三个正整数aj,bj,cj,表示aj 号和bj 号罪犯之间存在仇恨,其怨气值为cj。数据保证a b N j j 1 ≤ < ≤ ,0 < ≤ 1,000,000,000 j c ,且每对罪犯组合只出现一次。 【输出】 输出文件prison.out 共1 行,为Z 市长看到的那个冲突事件的影响力。如果本年内监狱 中未发生任何冲突事件,请输出0。 【样例输入】 4 6 1 4 2534 2 3 3512 1 2 28351 1 3 6618 2 4 1805 3 4 12884 【样例输出】 3512 【输入输出样例说明】 罪犯之间的怨气值如下面左图所示,右图所示为罪犯的分配方法,市长看到的冲突事件 影响力是3512(由2 号和3 号罪犯引发)。其他任何分法都不会比这个分法更优。 【数据范围】 对于30%的数据有N≤ 15。 对于70%的数据有N≤ 2000,M≤ 50000。 对于100%的数据有N≤ 20000,M≤ 100000。 【问题分析】 【分析】这道题的方法有很多,我是直接从逻辑关系入手,当然还有二分图。关键在于找出给出条件中矛盾的地方,我们假设所有给出的罪犯之间有矛盾,于是将其不放在同组,必然存在不能划分的状态,我们要让这个值最小,所以先将所有条件按矛盾值排序,然后逐个处理。考虑到监狱只有两个,产生了如下情况,若a与b不同组a与c不同组,那么bc不可同组,如果后面遇到了bc的条件,直接出结果。 一个算法显现出来。我们从最高矛盾值到最低处理,把ab矛盾的条件进行合并,a归于b且b归于a,他们同集就表明不可放到同组,如果出现c不可同组于a(b),则出解。 【源程序】 Program prison; Type point=array[1..3]of longint; Var n,m,i,x,y,ans:longint; c:array[0..100000]of point; fa:array[0..40000]of longint; procedure qsort(l,r:longint); var i,j,x:longint;y:point; begin i:=l;j:=r;x:=c[(i+j)div 2,3]; repeat while c[i,3]>x do inc(i); while x>c[j,3] do dec(j); if i<=j then begin y:=c[i];c[i]:=c[j];c[j]:=y; inc(i);dec(j); end; until i>j; if l if i end; function find(x:longint):longint; begin if x=fa[x] then exit(x); fa[x]:=find(fa[x]); exit(fa[x]); end; Begin readln(n,m); for i:=1 to m do readln(c[i,1],c[i,2],c[i,3]); qsort(1,m); for i:=1 to n*2 do fa[i]:=i; for i:=1 to m+1 do begin x:=find(c[i,1]); y:=find(c[i,2]); if x=y then break; fa[x]:=fa[find(c[i,2]+n)]; fa[y]:=fa[find(c[i,1]+n)]; end; writeln(c[i,3]); End. 【算法的实现】 对于并查集,我们可以采用的办法由两种:一种是同类合并。另一种是关系合并。 以下以关系合并为例,讲解以下并查集实现的过程: 0-代表同一个监狱(朋友) 1-代表不同的监狱(敌对) 对于当前这条待删去的边来说,我们需要判断边上所连接的两个点是否曾经有过关系,如果不曾有过关系,则说明这两个点可以成为敌对的关系;反之,需要我们判断当前这两点的关系是否敌对,如果不敌对则说明当前这条边是不可删去的,否则是可以删去的。 路径压缩的时候,我们需要修改点的父亲,以及点与父亲的关系。合并的时候,我们也需要建立一个点与其父亲的关系。(在个人标程中有详细的说明。) 【个人标程】 以下是个人标程,供大家参考: program prison; const maxn=20000; maxm=100000; var n,m:longint; f,s:array[0..maxn+1] of longint; a,b,c:array[0..maxm+1] of longint; function findsets(k:longint):longint; var y:longint; begin if f[k]<>k then begin y:=f[k]; f[k]:=findsets(y); {路径的压缩} s[k]:=(s[k]+s[y]) mod 2; {在路径压缩中正确的建立与新的父亲的新的关系} end; exit(f[k]); end; procedure combinesets(a,b:longint); begin s[f[a]]:=(s[a]+1) mod 2; {建立a的父亲与b的关系,1为敌人,0为朋友} f[f[a]]:=b; {改变a的父亲的对象,为b} end; procedure work; var i,sa,sb:longint; begin for i:=m downto 1 do {枚举每条边,进行二分图的判断} begin sa:=findsets(a[i]); sb:=findsets(b[i]); if sa=sb then begin if s[a[i]]=s[b[i]] then {当二者之前发生过关系,并且现在的关系和之前的关系相矛盾,则说明当前的为输出解} begin writeln(c[i]); exit; end; end else combinesets(a[i],b[i]); {否则将二者合并,建立关系} end; writeln(0);{删去所有的边后仍然符合条件,则说明此时应当输出0} end; procedure qsort(l,r:longint); {将读入的边按照大小排序} var i,j,x,y:longint; begin i:=l; j:=r; x:=c[(l+r) div 2]; repeat while c[i] while c[j]>x do dec(j); if not (i>j) then begin y:=a[i]; a[i]:=a[j]; a[j]:=y; y:=b[i]; b[i]:=b[j]; b[j]:=y; y:=c[i]; c[i]:=c[j]; c[j]:=y; inc(i); dec(j); end; until i>j; if j>l then qsort(l,j); if i end; procedure makesets; {建立一个新的并查集} var i:longint; begin for i:=1 to n do f[i]:=i; end; procedure reads; {读入数据} var i:longint; begin readln(n,m); makesets; for i:=1 to m do readln(a[i],b[i],c[i]); end; begin assign(input,'prison.in'); reset(input); assign(output,'prison.out'); rewrite(output); reads; qsort(1,m); work; close(input); close(output); end. 例4、联络员(TYVJ1307) 描述 Description Tyvj已经一岁了,网站也由最初的几个用户增加到了上万个用户,随着Tyvj网站的逐步壮大,管理员的数目也越来越多,现在你身为Tyvj管理层的联络员,希望你找到一些通信渠道,使得管理员两两都可以联络(直接或者是间接都可以)。Tyvj是一个公益性的网站,没有过多的利润,所以你要尽可能的使费用少才可以。 目前你已经知道,Tyvj的通信渠道分为两大类,一类是必选通信渠道,无论价格多少,你都需要把所有的都选择上;还有一类是选择性的通信渠道,你可以从中挑选一些作为最终管理员联络的通信渠道。数据保证给出的通行渠道可以让所有的管理员联通。 输入格式 Input Format 第一行n,m表示Tyvj一共有n个管理员,有m个通信渠道 第二行到m+1行,每行四个非负整数,p,u,v,w 当p=1时,表示这个通信渠道为必选通信渠道;当p=2时,表示这个通信渠道为选择性通信渠道;u,v,w表示本条信息描述的是u,v管理员之间的通信渠道,u可以收到v的信息,v也可以收到u的信息,w表示费用。 输出格式 Output Format 最小的通信费用 样例输入 Sample Input 5 6 1 1 2 1 1 2 3 1 1 3 4 1 1 4 1 1 2 2 5 10 2 2 5 5 样例输出 Sample Output 9 [分析]:这道题很明显用kruscal+并查集,因为有一些边是已经确定必须要加入的,所以可以先用一个预处理过程将这些边加入并且合并到一棵树上。在此基础之上再考虑贪边。 type way=record u,v,w:longint; end; var n,m,i,p,j,k,sum:longint; f:array[0..2010]of longint; a,b:array[0..10010]of way; procedure readdata; begin readln(n,m); j:=0; k:=0; for i:=1 to m do begin read(p); if p=1 then begin//必须路和非必须路分开处理 inc(j); readln(a[j].u,a[j].v,a[j].w); end else begin inc(k); readln(b[k].u,b[k].v,b[k].w); end; end; end; function find(x:longint):longint;//查找父节点 begin if f[x]<>x then f[x]:=find(f[x]); end; procedure merge(a,b:longint);//合并两棵树 var fa,fb:longint; begin fa:=find(a); fb:=find(b); f[fa]:=fb; end; procedure sort(l,r:longint);//快排,将非必须路由小到大进行排序 var i,j,x:longint; t:way; begin i:=l; j:=r; x:=b[(i+j)shr 1].w; repeat while b[i].w while b[j].w>x do dec(j); if i<=j then begin t:=b[i]; b[i]:=b[j]; b[j]:=t; inc(i); dec(j); end; until i>j; if i if l end; procedure pre_work;//预处理 begin sum:=0; for i:=1 to n do f[i]:=i; for i:=1 to j do begin sum:=sum+a[i].w; if f[a[i].u]<>f[a[i].v] then merge(a[i].u,a[i].v); end; sort(1,k); end; procedure main;//主过程kruscal begin for i:=1 to k do begin if find(b[i].u)<>find(b[i].v) then begin sum:=sum+b[i].w; merge(b[i].v,b[i].u); end; end; writeln(sum); end; begin readdata; pre_work; main; end. [另解]:如果是必选,直接加入答案,同时把它的权值改成0,然后直接对m条边Kruscal,对于环,其实并查集也可以处理。于是又加以一番改造,代码短了不少,时间也快了很多。看来一个好的算法真的是各方面都十分优异啊! type zj=record x,y,z:longint; end; var n,m,ans:longint; e:array[1..10000] of zj; a:array[1..10000] of longint; k,i,p,q:longint; procedure qsort(l,r:longint); var i,j,x:longint; m,k:zj; begin i:=l; j:=r; x:=(l+r) shr 1; m:=e[x]; repeat while e[i].z while e[j].z>m.z do dec(j); if i<=j then begin k:=e[j]; e[j]:=e[i]; e[i]:=k; inc(i); dec(j); end; until i>j; if i if l end; function find(x:longint):longint; var k:longint; begin k:=x; while a[k]<>0 do k:=a[k]; exit(k); end; begin readln(n,m); ans:=0; for i:=1 to m do begin read(k); readln(e[i].x,e[i].y,e[i].z); if k=1 then begin inc(ans,e[i].z); e[i].z:=0; end; end; qsort(1,m); fillchar(a,sizeof(a),0); for i:=1 to m do begin q:=find(e[i].x); p:=find(e[i].y); if p<>q then begin inc(ans,e[i].z); a[p]:=q; end; end; writeln(ans); end. 例题5、食物链(noi2001) 【问题描述】 动物王国中有三类动物 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),输出假话的总数。 【输入】 第一行是两个整数 N和K,以一个空格分隔。 以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。 若D=1,则表示X和Y是同类。 若D=2,则表示X吃Y。 【输出】 只有一个整数,表示假话的数目。 【样例输入】 100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5 【样例输出】 3 【问题分析】 由于每次要判断元素是否已经被包含在某个集合中,这让我们想到了并查集。 但这道题目,并不是一个简单的并查集操作,因为还要判断这些点之间吃与被吃的关系。 由此,这道题就变成了一个带权并查集的经典应用。 首先,带权并查集是在并查集的基础上,加上一个权值,表示节点与根的关系。 拿这道题来说明,在同样使用并查集的情况下,我们可以对每一个节点加上一个权值,表示该叶节点与根节点的关系。 设f[i]=0表示节点i与根节点是同类的动物。 F[i]=1表示节点i是吃根节点的动物。 F[i]=2表示节点i是被根节吃的动物。 那么,在同样压缩路径的时候就要注意一点,对于每一次压缩路径,我们都要重新改变f的值,这样才能保证在压缩路径的时候,不会弄错动物们之间的关系。 下面是路径压缩得代码 function find(i:longint):longint; var k:longint; begin if r[i]=i then exit(r[i]); k:=r[i]; 由于父节点会在下一步发生改变,所以要记录下该节点原来的父节点 r[i]:=find(r[i]); 正常并查集的路径压缩 f[i]:=(f[i]+f[k])mod 3; 因为根节点的改变,所以需要重新改变该节点和根结点的关系 exit(r[i]); end; 对于这一步的解释 f[i]:=(f[i]+f[k])mod 3 我是把所有情况全部罗列出来,推出的公式(虚线表示刚刚推出来的(也就是路径压缩后的结果) 下面的图表示 2是根节点 1的根节点是2; 3原本的根节点属于1在路径压缩后变成2 边长的权值表示也节点和根节点的关系。例:f[1]=0表示 1和2是同种动物 一上9个图形描述了所有路径压缩的可能情况,于是可以的到的结论是路径压缩后的f[i]=(f[i]+f[k])mod 3; 剩下的就是合并的问题了。 合并的时候,我们是把根合并,那么自然,相应的根节点之间的关系也应该发生改变 一下是合并的代码 procedure combine(x,y,s:longint); var u,v:longint; begin u:=find(x); v:=find(y); 找到改节点的根节点 r[u]:=v; 合并根节点 f[u]:=(f[y]+s-f[x]+3)mod 3; 改变根节点之间的关系 end; f[u]:=(f[y]+s-f[x]+3)mod 3; 用同样的方法推出来的,共有27种情况,这里就不一一列举了,随意举出一种情况。如图、 (此时输入 2 3 4)需要合并的根是 1 和 2 加粗表示需要合并的点(3 4)(不一定是根节点) 此时的f[1]=1; 编号是2的节点变成了所有节点的根。 整个压缩路径和合并集合的过程解决以后,整到题目就很简单了。 procedure main; var i,j,d,u,v,r1,r2:longint; begin readln(n,m); for i:=1 to n do r[i]:=i; for i:=1 to m do begin readln(d,u,v); if (u>n)or(v>n) then 判断条件2是否成立 begin inc(ans); continue; end; r1:=find(u); r2:=find(v); case d of 1: begin if r1<>r2 then combine(u,v,0) 不在同一集合要合并 else if f[u]<>f[v] then inc(ans); 同种动物 属于同一种集合 但对根节点的意见不同 end; 所以断定是谎言 2: begin if u=v then 判断条件3 begin inc(ans); continue; end; if r1=r2 then begin if (f[u]+2) mod 3<>f[v] then inc(ans) 条件不满足相互吃 判断是否是假话,和 end 上面的一样,要推一下公式 else combine(u,v,1); 不在同一集合 要合并 end; end; end; end; 源码: var r,f:array[0..50000] of longint; n,m,ans:longint; function find(i:longint):longint; var k:longint; begin if r[i]=i then exit(r[i]); k:=r[i]; r[i]:=find(r[i]); f[i]:=(f[i]+f[k])mod 3; exit(r[i]); end; procedure combine(x,y,s:longint); var u,v:longint; begin u:=find(x); v:=find(y); r[u]:=v; f[u]:=(f[y]+s-f[x]+3)mod 3; end; procedure main; var i,j,d,u,v,r1,r2:longint; begin readln(n,m); for i:=1 to n do r[i]:=i; for i:=1 to m do begin readln(d,u,v); if (u>n)or(v>n) then begin inc(ans); continue; end; r1:=find(u); r2:=find(v); case d of 1: begin if r1<>r2 then combine(u,v,0) else if f[u]<>f[v] then inc(ans); end; 2: begin if u=v then begin inc(ans); continue; end; if r1=r2 then begin if (f[u]+2) mod 3<>f[v] then inc(ans); end else combine(u,v,1); end; end; end; end; begin main; writeln(ans); end. 参考程序: program eat; const MaxN=50000; var f,t:array[1..MaxN] of longint; i,n,k,d,x,y,a,b,ans:longint; function father(x:longint):longint; begin if f[x]=0 then exit(x); father:=father(f[x]); t[x]:=(t[f[x]]+t[x]) mod 3; f[x]:=father; end; function SameType:boolean; begin if x=y then exit(true); a:=father(x); b:=father(y); if a=b then exit(t[x]=t[y]); f[b]:=a; t[b]:=(t[x]-t[y]+3) mod 3; end; function XEatY:boolean; begin if x=y then exit(false); a:=father(x); b:=father(y); if a=b then exit(t[y]=(t[x]+1) mod 3); f[b]:=a; t[b]:=(t[x]-t[y]+4) mod 3; end; begin readln(n,k); ans:=k; for i:=1 to k do begin readln(d,x,y); if (x>n) or (y>n) then continue; case d of 1:if SameType then dec(ans); 2:if XEatY then dec(ans); end; end; writeln(ans); end. 总结 这道题目考的是带权并查集,整道题的难点是推出的那些满足条件的公式,也就是权值之间的转换。 例题6、银河英雄传说(noi2002) 【问题描述】 公元五八○一年,地球居民迁移至金牛座α第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。 宇宙历七九九年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。 杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成30000列,每列依次编号为1, 2, …,30000。之后,他把自己的战舰也依次编号为1, 2, …, 30000,让第i号战舰处于第i列(i = 1, 2, …, 30000),形成“一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为M i j,含义为让第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。 在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。 最终的决战已经展开,银河的历史又翻过了一页…… 【输入文件】 输入文件galaxy.in的第一行有一个整数T(1<=T<=500,000),表示总共有T条指令。 以下有T行,每行有一条指令。指令有两种格式:1. M i j :i和j是两个整数(1<=i , j<=30000),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第i号战舰与第j号战舰不在同一列。2. C i j :i和j是两个整数(1<=i , j<=30000),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。 【输出文件】 输出文件为galaxy.out。你的程序应当依次对输入的每一条指令进行分析和处理:如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息;如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第i 号战舰与第j 号战舰之间布置的战舰数目。如果第i 号战舰与第j号战舰当前不在同一列上,则输出-1。 【样例输入】 4 M 2 3 C 1 2 M 2 4 C 4 2 【样例输出】 -1 1 【分析】 对于第1、2条,我们立刻可以想到并查集。但是第三条呢?那可是并查集没有提供的操作啊!难道我们要换数据结构?可是一时又想不到其它什么好的数据结构能完美支持前2个操作。不得已,我们想到了扩展并查集。 普通并查集中的每个元素都有个值记录它的父亲节点(树形并查集)。为了实现第三个操作,我们再给每个元素i添加2个值d[i]和l[i]。d[i]表示它在它所处集合中的深度(即离根的距离),d[root]=0;l[i]表示以i为根的集合中的元素个数,它当且仅当i是根时有意义。 查找一个元素x所在集合的代表元时,先从x沿着父亲节点找到这个集合的代表元root,然后再从x开始一次到root的遍历,对于在遍历中的每个节点,更新它的d值并实行“路径压缩”优化。 [参考程序] type ty=record root,deep,sons:longint; end; var a:Array[1..50000]of ty; T,CtrlN1,CtrlN2:longint; ch,temp:char; i:longint; procedure Main(CtrlMsg:char); var f1,f2:longint; function FindRoot(target:longint):longint; var p:longint; begin if a[target].root=target then begin exit(target); end; p:=FindRoot(a[target].root); a[target].deep:=a[a[target].root].deep+a[target].deep; a[target].root:=p; exit(p); end; begin case CtrlMsg of 'M': begin //找到root,并在退出递归的时候把所经过节点的root值改为其实际的root f1:=FindRoot(CtrlN1); f2:=FindRoot(CtrlN2); a[f1].deep:=a[f2].sons+a[f1].deep; //把CtrlN1的root的deep改为CtrlN2的root的deep+CtrlN1的root的deep a[f1].root:=f2; //把CtrlN1的root的root改为CtrlN2的root(root指的是当前节点的最终祖先) a[f2].sons:=a[f2].sons+a[f1].sons; //把CtrlN2的root的sons改为CtrlN2的root的sons+CtrlN1的root的sons(sons指的是当前节点(须为根节点)的链的深度) end; 'C': begin f1:=FindRoot(CtrlN1); f2:=FindRoot(CtrlN2); if f1<>f2 then writeln(-1) else writeln(abs(a[CtrlN1].deep-a[CtrlN2].deep)-1); end; end; end; begin assign(input,'galaxy.in');reset(input); assign(output,'galaxy.out');rewrite(output); readln(T); for i:=1 to 30000 do begin a[i].root:=i; a[i].deep:=0; a[i].sons:=1; end; for i:=1 to T do begin read(ch,temp); read(CtrlN1,CtrlN2); readln; Main(ch); end; close(input); close(output); end. 【Pascal源码】:此程序最后两个点超时; const function find(x:integer):integer; procedure union(a,b:integer); {=========main========} 题解二 var Father,Count,Behind:array[1..maxn] of integer; 在同一队列中的战舰组成一个树型结构的并查集。 Father[x]— 战舰x的父指针。Father[x]=x,表明战舰x为根节点。路径压缩后,树中所有子节点的父指针指向根节点; Count[x]— 以节点x为根的树的节点数; Behind[x]— 战舰x在列中的相对位置; 初始时,我们为每一艘战舰建立一个集合,即 Father[x]=x Count[x]=1 Behind[x]=0(1≤x≤30000) 1、查找根节点并进行路径压缩 function Find_Father(x:integer):integer;{查找节点x所在树的根节点,并进行路径压缩} var i,j,f,next:integer; begin i←x; {找出节点x所在树的根节点f} while Father[i]<>i do i←Father[i]; f←i;i←x; while i<>f do {按照自下而上的顺序处理x的祖先节点} begin next←Father[i];Father[I]←f;{把节点i的父节点设为f,完成路径压缩} j←next; repeat Behind[i]←Behind[i]+Behind[j];{迭代求出路径上每一个子节点相对于f的相对位置} j←Father[j]; until Father[j]=j; i←next; end;{while} find_Father←f; {返回x所在树的根节点} end;{ Find_Father } 2、计算合并指令 procedure MoveShip(x,y:integer);{把x所在的集合合并入y所在的集合} var fx,fy:integer; begin fx←find_Father(x);{计算x所在树的根节点fx} fy←find_Father(y);{计算y所在树的根节点fy} Father[fx]←fy;{将fx的父节点设为fy} Behind[fx]←Count[fy];{计算fx的相对位置为Count[fy]} Count[fy]←Count[fy]+Count[fx];{计算新集合的节点数} end;{ MoveShip } 3、计算询问指令 procedure CheckShip(x,y:integer);{计算x节点和y节点的相对位置情况} var f1,f2:integer; begin f1←Find_Father(x);{计算x所在树的根f1} f2←Find_Father(y);{计算y所在树的根f2} if f1<>f2 {若x,y不在一棵树中,则返回-1,否则返回x和y间隔的战舰数} then writeln(-1) else writeln(abs(Behind[x]-Behind[y])-1); end;{ CheckShip } 由此得出主程序: for i←1 to maxn do {初始时为每一艘战舰建立一个并查集} begin Father[i] ←i; Count[i] ←1; Behind[i] ←0; end;{for} readln(CmdCount); {读指令数} for i←1 to CmdCount do {顺序处理每一条指令} begin read(ch);{读第i条指令的类型} case ch of 'M':begin readln(x,y);MoveShip(x,y); end;{处理合并指令} 'C':begin readln(x,y);CheckShip(x,y); end;{处理询问指令} end;{case} end;{for}
读完题,看到T那庞大的规模,经过简单分析题目要求的东西,可以知道需要我们维护一个数据结构,使之支持:
1、合并集合(M i j)
2、查询2个元素是否在同一集合(C i j)
3、如果在同一集合,求出它们的相对位置
合并2个集合后,更新一下d和l值即可。具体见程序。
maxn=30000;
type
node=record
p,d,l :integer;
end;
var
uf :array[1..maxn]of node; //并查集
n,m :longint;
a,b,i :longint;
c :char;
procedure init;
var
i :integer;
begin
assign(input,'galaxy.in');reset(input);
assign(output,'galaxy.out');rewrite(output);
n:=30000;
readln(m);
for i:=1 to n do //初始化并查集:p[i]=i,l[i]=1
with uf[i] do begin p:=i; l:=1 end;
end;
var
i,j,p,q :integer;
begin
i:=x;
while uf[i].p<>i do i:=uf[i].p; //查找代表元
p:=i;
i:=x;
while i<>p do
begin
q:=uf[i].p;
uf[i].p:=p; //路径压缩
j:=q;
repeat
inc(uf[i].d,uf[j].d); //更新d值
j:=uf[j].p
until uf[j].p=j;
i:=q;
end;
find:=p;
end;
var
t :integer;
begin
a:=find(a); b:=find(b);
uf[a].p:=b; //合并
uf[a].d:=uf[b].l; //更新d
inc(uf[b].l,uf[a].l) //更新l
end;
begin
init;
for i:=1 to m do
begin
readln(c,a,b);
if c='M' then union(a,b);
if c='C' then
if (find(a)=find(b)) then
writeln(abs(uf[a].d-uf[b].d)-1)
else writeln(-1)
end;
close(output)
end.