说道并查集,大家一定对于以多叉树状结构为基础的并查集并不陌生,最常见的两种写法如下
1 function getfat(x:longint):longint; 2 begin 3 while x<>c[x] do x:=c[x]; 4 exit(x); 5 end;
1 function getfat(x:longint):longint; 2 begin 3 if x<>c[x] then exit(getfat(c[x])) else exit(x); 4 end;
其实并查集的核心操作就是一个寻根操作。代码都很短,也很不容易写错,原理也很明显——假如两个点已经到了一个块里的话,那么由于在树结构中,则两者必然有个共同的祖先节点——根节点。。。于是我就这样子学会了并查集
直到某一天在网上看到某神犇在Pascal程序中采用了如下写法——
1 function getfat(x:longint):longint; 2 begin 3 if x<>c[x] then c[x]:=getfat(c[x]); 4 exit(c[x]); 5 end;
这个里面的程序和之前的第一段程序相当类似,但有着很大的区别——这个里面不单单是用递归的方式找到了根节点,同时又变更着各个中间各级父亲节点的父节点——实际上都变成了树的根节点,也就是说每个getfat操作之后,得到的树在此点所涉及的支链上都已经被撸到了根节点上(HansBug:喂喂喂别想歪了),这样子对于一再的重复操作可以起到减少树的高度的神奇作用。
说了这些,于是来一个小小的实验比对下——
准备:一个并查集程序,实现功能——输入N、M、T,表示原来有N堆,M次操作,T(1表示用循环并查集;2表示用简单递归并查集;3表示用最下面的新方法);接下来输入M行,每行"z x y","1 x y"表示将x和y所在块合并,"2 x y"表示查看x和y在不在一起。就是这样一个简单的问题
程序如下:(本程序中均不开inline优化,同时均在我家的烂电脑上进行测试,速度比较逗——这么告诉你们吧,我曾经不止一次提心吊胆地把我在本机上单组数据就运行了快2秒的程序交上BZOJ,结果一瞬间AC,时间是1000ms多一点,吓尿了)
1 var 2 i,j,k,l,m,n,t:longint; 3 c:array[0..1000000] of longint; 4 function getfat1(x:longint):longint; 5 begin 6 while x<>c[x] do x:=c[x]; 7 exit(x); 8 end; 9 function getfat2(x:longint):longint; 10 begin 11 if x<>c[x] then exit(getfat2(c[x])) else exit(x); 12 end; 13 function getfat3(x:longint):longint; 14 begin 15 if x<>c[x] then c[x]:=getfat3(c[x]); 16 exit(c[x]); 17 end; 18 function getfat(x,y:longint):longint; 19 begin 20 case y of 21 1:exit(getfat1(x)); 22 2:exit(getfat2(x)); 23 3:exit(getfat3(x)); 24 end; 25 end; 26 begin 27 readln(n,m,t); 28 for i:=1 to n do c[i]:=i; 29 for i:=1 to m do 30 begin 31 readln(j,k,l); 32 case j of 33 1:c[getfat(k,t)]:=c[getfat(l,t)]; 34 2:writeln(c[getfat(k,t)]=c[getfat(l,t)]); 35 end; 36 end; 37 end.
于是开始很开心的对拍:
1.N=1000 M=1000000 —— 结果发现,循环实现的并查集居然最好,大约1.2s左右,然后两种递归实现的并查集都差不多少,大约都1.3s左右,呵呵呵呵。。。神奇
2.N=100 M=10000000 ——现在的数据一定更容易产生高大的树,一方面合并增多了,另一方面原始的块数少了,也就意味着块会更加集中。。。
于是结果是——最新的方法明显强大,大约8-10秒,而这个时候循环实现的并查集还是比递归的快,大约12s,而递归的就要至少13s啦.。。
其实——在原来的并查集中每次合并操作(c[getfat(x)]:=getfat(y);)都已经将一棵树的根节点直接连接到了另一棵树的根节点上,两棵树实现了简单合并,但是对于两点是否在同一块内的访问操作时,原来的程序没有减少高度,也就是说对于同一个树的状态访问多少次都是一样要跑那么高,这样子完全可以构造出很高大的一棵树(通过节节合并不难构造出),每次都会爬好高的树,当然慢啦。。。而这个将每次的树直接缩节后,那么高的树就只需要跑一次了,以后直接一下子就升到根节点上啦!!!哥就是这么霸气!!!
总结:事实证明了新方法的强大,在关于树的问题上,对于树结构的不断优化也必将不断提高算法的速度——这是关于树的问题亘古不变的一个定则!!!最重要的是有get了一个新技能,可以更开心的刷题啦哈哈哈哈^_^
(附1:随机数生成器)
1 var 2 i,j,k,l,m,n:longint; 3 a:array[0..10000000,1..3] of longint; 4 begin 5 readln(n,m); 6 randomize; 7 for i:=1 to m do 8 begin 9 a[i,1]:=random(2)+1; 10 a[i,2]:=random(n)+1; 11 a[i,3]:=random(n)+1; 12 end; 13 assign(output,'bxj1.txt'); 14 rewrite(output); 15 writeln(n,' ',m,' ',1); 16 for i:=1 to m do writeln(a[i,1],' ',a[i,2],' ',a[i,3]); 17 close(output); 18 assign(output,'bxj2.txt'); 19 rewrite(output); 20 writeln(n,' ',m,' ',2); 21 for i:=1 to m do writeln(a[i,1],' ',a[i,2],' ',a[i,3]); 22 close(output); 23 assign(output,'bxj3.txt'); 24 rewrite(output); 25 writeln(n,' ',m,' ',3); 26 for i:=1 to m do writeln(a[i,1],' ',a[i,2],' ',a[i,3]); 27 close(output); 28 end.
(附2:bat小程序,用于对拍,fuckbxj.exe表示随机数生成器)
1 @echo off 2 set /a s=0 3 :1 4 set /a s=s+1 5 echo Test %s% 6 rem 此处两个数分别表示N和M 7 echo 100 10000000|fuckbxj.exe 8 echo 循环求并查集 9 echo.|time 10 type bxj1.txt|bxj.exe >bxj1.out 11 echo.|time 12 echo 递归求并查集 13 echo.|time 14 type bxj2.txt|bxj.exe >bxj2.out 15 echo.|time 16 echo 优化递归并查集 17 echo.|time 18 type bxj3.txt|bxj.exe >bxj3.out 19 echo.|time 20 fc bxj1.out bxj2.out 21 fc bxj1.out bxj3.out 22 pause 23 goto 1