【转】关于并查集

poj 题目
1861
1182 (难)
1308

下面是王建德的讲义:

如果:给出各个元素之间的联系,要求将这些元素分成几个集合,每个集合中的元素直接或间接有联系。在这类问题中主要涉及的是对集合的合并和查找,因此将这种集合称为并查集。
链表被普通用来计算并查集.表中的每个元素设两个指针:一个指向同一集合中的下一个元素;另一个指向表首元素。

链结构的并查集

采用链式存储结构,在进行集合查找时的算法复杂度仅为O(1);但合并集合时的算法复杂度却达到了O(n)。如果我们希望两种基本操作的时间效率都比较高的话,链式存储方式就“力不从心”了。

树结构的并查集

采用树结构支持并查集的计算能够满足我们的要求。并查集与一般的树结构不同,每个顶点纪录的不是它的子结点,而是将它的父结点记录下来。下面是树结构的并查集的两种运算方式

⑴直接在树中查询
⑵边查询边“路径压缩”

对应与前面的链式存储结构,树状结构的优势非常明显:编程复杂度低;时间效率高。

直接在树中查询

集合的合并算法很简单,只要将两棵树的根结点相连即可,这步操作只要O(1)时间复杂度。算法的时间效率取决于集合查找的快慢。而集合的查找效率与树的深度呈线性关系。因此直接查询所需要的时间复杂度平均为O(logN)。但在最坏情况下,树退化成为一条链,使得每一次查询的算法复杂度为O(N)。

边查询边“路径压缩

其实,我们还能将集合查找的算法复杂度进一步降低:采用“路径压缩”算法。它的想法很简单:在集合的查找过程中顺便将树的深度降低。采用路径压缩后,每一次查询所用的时间复杂度为增长极为缓慢的ackerman函数的反函数——α(x)。对于可以想象到的n,α(n)都是在5之内的。

下面是两道例题(是用pascal写的):

1。银河英雄传说
【问题描述】公元五八○一年,地球居民迁移至金牛座α第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。
宇宙历七九九年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。
杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成30000列,每列依次编号为1, 2, …, 30000。之后,他把自己的战舰也依次编号为1, 2, …, 30000,让第i号战舰处于第i列(i = 1, 2, …, 30000),形成“一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为Mij,含义为让第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。
在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:Cij。该指令意思是,询问电脑,杨威利的第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。 作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。
最终的决战已经展开,银河的历史又翻过了一页……
【输入文件】
输入文件galaxy.in的第一行有一个整数T(1≤T≤500,000),表示总共有T条指
以下有T行,每行有一条指令。指令有两种格式:
1. Mij:i和j是两个整数(1≤i , j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第i号战舰与第j号战舰不在同一列。
2.Cij:i和j是两个整数(1≤i , j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。
【输出文件】
输出文件为galaxy.out。你的程序应当依次对输入的每一条指令进行分析和处理:
如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息;
如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第i号战舰与第j号战舰之间布置的战舰数目。如果第i号战舰与第j号战舰当前不在同一列上,则输出-1。

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}

2。最长下降序列
给定一个整数序列a,求最长下降序列的长度。设m[i]为ai的前缀中(包括ai )的最长下降序列的长度。该序列中ai前的整数为ap[i]
对P进行特殊的限制,即在所有等价的(相同长度)决策j中,P选择a[j]最大的那一个
在处理完a[1..x-1]之后,对于所有长度为M[x]-1的下降序列,P[x]的决策只跟其中末尾最大的一个有关。
用另外一个动态变化的数组b,当我们计算完了a[x]之后,a[1..x]中得到的所有下降序列按照长度分为L类,每一类中只需要一个作为代表,这个代表在这个等价类中末尾的数最大,我们把它记为b[j],1≤j≤L。
处理当前的一个数a[x],我们无需和前面的a[j](1≤j≤x-1)作比较,只需要和b[j](1≤j≤L)进行比较。
首先,如果a[x]<b[L],把a[x]接在这个序列的后面,形成了一个长度为L+1的序列.。这时b[L+1]=a[x],即a[x]作为长度为L+1的序列的代表,同时L应该增加1。
另一种可能是a[x]>=b[1],这时a[x] 仅能构成长度为1的下降序列,同时它又必然是最大的,所以它作为b[1]的代表,b[1]=a[x]。
如果前面的情况都不存在,我们肯定可以找到一个j,2≤j≤L,有b[j-1]>a[x],b[j]≤a[x],这时分析,a[x]必然接在b[j-1]后面,行成一个新的长度为j的序列。
这几种情况实际上都可以归结为:处理a[x],令b[L+1]为无穷小,从左到右找到第一个位置j,使b[j]≤a[x],然后则只要将b[j]=a[x],如果j=L+1,则L同时增加。x处以前对应的最长下降序列长度为M[x]=j。

顺序查找
L ←0; {下降序列的最大长度初始化}
for x←1 to n do
begin
b[L+1]←无穷小
j←1{从左到右找到第一个使得b[j]≤a[x]的位置j}
while (b[j]>a[x]) do j←j+1
b[j]←a[x]
if j>L then L←j
end
顺序查找更改成二分法查找
L←0 {下降序列的最大长度初始化}
for i←1 to n do
begin
j←1;k←L;{在b[1]..b[L]中二分法查找第一个使得b[j]≤a[x]的位置j}
while (j≤k) do{改为二分法查找}
begin
m ← (j+k) div 2;
if b[m]>a[i] then j←m+1 else k←m-1;
end
if j>L then L←L+1;b[j]←a[i]
end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值