左偏树+并查集 Monkey King -- ZJU 2334

探讨使用并查集和左偏树解决猴子冲突问题,包括输入输出样例及算法实现细节。
Monkey King

Time limit: 10 Seconds   Memory limit: 32768K  
Total Submit: 1511   Accepted Submit: 390  

Once in a forest, there lived N aggressive monkeys. At the beginning, they each does things in its own way and none of them knows each other. But monkeys can't avoid quarrelling, and it only happens between two monkeys who does not know each other. And when it happens, both the two monkeys will invite the strongest friend of them, and duel. Of course, after the duel, the two monkeys and all of their friends knows each other, and the quarrel above will no longer happens between these monkeys even if they have ever conflicted.

Assume that every money has a strongness value, which will be reduced to only half of the original after a duel(that is, 10 will be reduced to 5 and 5 will be reduced to 2).

And we also assume that every monkey knows himself. That is, when he is the strongest one in all of his friends, he himself will go to duel.


Input

There are several test cases, and each case consists of two parts.

First part: The first line contains an integer N(N<=100,000), which indicates the number of monkeys. And then N lines follows. There is one number on each line, indicating the strongness value of ith monkey(<=32768).

Second part: The first line contains an integer M(M<=100,000), which indicates there are M conflicts happened. And then M lines follows, each line of which contains two integers x and y, indicating that there is a conflict between the Xth monkey and Yth.


Output

For each of the conflict, output -1 if the two monkeys know each other, otherwise output the strongness value of the strongest monkey in all friends of them after the duel.

Sample Input

5
20
16
10
10
4
5
2 3
3 4
3 5
4 5
1 5

Sample Output

8
5
5
-1
10

 解题报告

从题目的描述可以看出,需要判断两只猴子是否属于同一集合并且将不同集合的猴子合并为一个集合,这我们可以想到并查集。同时我们需要求出一个集合中力量最大的猴子,将力量最大猴子的力量减半,然后将两个集合进行合并并重新选择力量最大的猴子。开始我用的是二叉堆,结果就是TLE。原因在于在一个堆中逐个插入元素并进行调整的时间复杂度为O(nlogn)。因此需要一种支持堆之间快速合并操作的可合并堆。这我们可以选用两种:左偏树以及二项堆。由于二项堆过于复杂,在此我选用的是左偏树。每个并查集合的根节点记录集合对应左偏树的根节点。具体步骤如下:

1.初始化并查集和左偏树;

2 .猴子x与y之间有conflict

    2.1 利用并查集判断x与y是否在同一个集合中,如果是,输出-1;

     2.2 通过两只猴子所在并查集集合的根节点获取猴子所在的左偏树的根节点hx和hy。hx和hy的力量减半;

     2.3 合并hx和hy。由于hx和hy的力量减半,因此hx和hy可能已不满足左偏树的性质(节点的键值大于等于左右子    树        根节点的键值),不能直接合并。我最先采用的方法是写个下降调整的函数,先将两边调整为左偏树,然后合  并,但是老TLE,最后不得不换成另外一种方法:先将hx和hy从各自所在的左偏树中去掉,然后将hx的左右子树合并形成左偏树nx,hy的左右子树合并形成左偏树mx,接着将mx合并到nx;hx和hy从所在左偏树去掉后,形成两棵只有一个节点的左偏树,将hy合并到hx,最后hx合并到nx就形成了一颗新的左偏树。虽然这样比较繁琐,但是可以看到这若干步合并操作总的时间复杂度只有log(n)+log(m),n和m分别是原来两颗左偏树的节点个数。第一种方法的时间复杂度就多出了下降调整的开销log(n)+log(m),因此时间开销是第二种方法的两倍。

     2.4 合并两只猴子所在的并查集集合,并重新记录集合中力量最大的猴子。

代码:

#include

<stdio.h>

#include

<string.h>

#define

MAXN 100000

typedef

struct

{

 

 

 

 

}HeapNode;

int val;int dist;int left;int right;

static

HeapNode H[MAXN+1];

static

int parent[MAXN+1];

static

int heapRef[MAXN+1];

int

{

 

 

 

t = x;

 

{

k = parent[t];

parent[t] = root_x;

t = k;

}

 

}

Find_Set(int x)int root_x;int t,k;for(root_x = x; parent[root_x]>=0; root_x = parent[root_x]);while(t!=root_x)return root_x;

int

{

 

 

{

parent[x] += parent[y];

parent[y] = x;

p = x;

}

 

Union(int x, int y)int p;if(parent[x] < parent[y])else

{

parent[y] += parent[x];

parent[x] = y;

p = y;

}

 

}

return p;

//ri<-rj

static

{

 

 

 

 

 

{

 

ri = rj;

rj = temp;

}

H[ri].right = merge(H[ri].right,rj);

 

 

 

{

 

H[ri].left = H[ri].right;

H[ri].right = temp;

}

H[ri].dist = H[right].dist + 1;

 

}

int merge(int ri, int rj)if(ri == 0)return rj;if(rj == 0)return ri;if(H[ri].val < H[rj].val)int temp = ri;int left = H[ri].left;int right = H[ri].right;if(H[left].dist < H[right].dist)int temp = H[ri].left;return ri;

int

{

 

H[0].dist = -1;

H[0].val = -1;

 

 

{

 

{

scanf(

H[i].left = H[i].right = 0;

H[i].dist = 0;

parent[i] = -1;

heapRef[i] = i;

}

scanf(

 

{

scanf(

rx = Find_Set(x);

ry = Find_Set(y);

 

{

printf(

 

}

 

hx = heapRef[rx];

hy = heapRef[ry];

H[hx].val/=2;

H[hy].val/=2;

hxl = H[hx].left;

hxr = H[hx].right;

hyl = H[hy].left;

hyr = H[hy].right;

nx = merge(hxl,hxr);

mx = merge(hyl,hyr);

nx = merge(nx,mx);

H[hx].left = H[hx].right = 0;

H[hx].dist = 0;

H[hy].left = H[hy].right = 0;

H[hy].dist = 0;

hx = merge(hx,hy);

nx = merge(nx,hx);

j = Union(rx,ry);

heapRef[j] = nx;

printf(

}

}

 

}

欢迎各位指正!

网上别人给出了二项堆的做法,不过比较复杂,我把它贴在下面:

Zju 2334 monkey king [二项堆]解题报告
 
这题应用到了一个高级数据结构:二项堆。
我对二项堆做一个介绍:
它的名字中包含堆,顾名思义它包含了所有对所具有的功能,能在log n时间内求出堆中极值,也支持对某个节点作相应的修改,除此之外它还支持堆与堆之间的合并,这点给我们解决此问题提供了途径。聪明的读者已经想到了,我们让认识的猴子作为一个堆中的成员,打仗不过是我们要实现的堆与堆的合并。
说到这,你可能会产生疑问,既然这二项堆它支持可并优先队列,那么它究竟拥有的是什么样的结构呢?现在我来为你解答这个问题。
我们都知道二项式,了解二进制,我们来看一些例子:
1 (10  = 12);
2 (10)   = 10 (2);
4 (10  = 1002);
8 (10)   = 1000  (2);
……
       在二进制定义中,有这样一件奇妙的事情:1+1 = 10 10 + 10 = 100;……我们实质上是重述了二进制加法的定义。
       我们顺次把二进制的数位从右到左依次编号为12,……,p,……我们发现在第p位的两个1和在p+1位的一个1是等价的。
       了解了以上这些事,大家也就知道了什么是二项树。下面是明确的定义:
              二项树 Bk的定义是递归的定义的有序树。它有一个最大深度 k和堆的定义一样,也和上面我们说的二进制中的p一样。Bk 由两个二项树Bk-1 构成。设这两个树分别为BmBn我们连接它们的方式就是Bm的左儿子这项的是BnBn的右兄弟指向的是原Bm的左儿子。这样我们看两个Bk-1和一个Bk就是等价的了。
那什么优势二项堆呢?
考虑:3 10= 112 = 1 + 10 510= 1012 = 1 + 100 ;……
我们如此定义二项堆:把若干个深度不同的二项树,按其深度从小到大连接起来形成队列,此队列称为二项堆。
如何做堆与堆的合并?简单说来我们先合并两个队列在合并队列中相邻的深度相同的点。
例如: 101 + 111 = 1 + 10 + 1 + 100 + 100
=1 + 1 + 10 + 100 + 100
= 10 + 10 + 100 + 100
= 100 + 100 + 100
=  ( 100 + 1000)
做题的目的是学习算法,而学习算法的最好途径还是匹配相应的习题。
下面我们结合程序来给出二项堆的一些基本结构:
这是结构:
Point= Longint;
Point 最为指针类型存在
pointtype=record
        father,leftchild,rightsub,setfather     :Point;
我们采取左儿子右兄弟的结构存储,并应用到了并查集setfather为节点的集合父亲。
        degree,power                            :Longint;
以此节点以下构成的子堆的深度,这个节点代表的猴子的强壮值。
end;
PROGRAM p2334;
CONST
        maxn=1000000;    // 最大范围
TYPE
        Point= Longint;
        pointtype=record
                father,leftchild,rightsub,setfather     :Point;
                degree,power                            :Longint;
        end;
VAR
        step                            :Integer;
        n,m                             :Longint;
        data                            :array[1..maxn]of Pointtype;
                            // 可并优先队列的表
PROCEDURE MakeNull;  // 初始化
var
        i                               :Longint;
begin
readln(n);
for i:=1 to n do
        with data[i] do
                begin
                father:=0;
                leftchild:=0;
                rightsub:=0;
                setfather:=0;
                degree:=0;
                readln(power);
                end;
end;
FUNCTION maxmember(a:point):Point;  // 求名称为a的二项堆中最大值所在的结点编号我们肯定他一定在一系列二项树的根节点上我们是通过a的右兄弟将其连接起来的
var
        max                             :Longint;
        p                               :Point;
begin
max:=-maxn;
repeat
        if data[a].power>max then
                begin
                max:=data[a].power;
                p:=a;
                end;
        if data[a].rightsub<>0 then a:=data[a].rightsub else break;
until false; // 枚举了每一个二项树的到最大值所在点的编号p
maxmember:=p;
end;
FUNCTION Find(p:point):point; // 寻找p所在二项堆的根节点并查结
var
        d,temp                          :Point;
begin
d:=p;
while (data[p].setfather<>0) do p:=data[p].Setfather;
while (d<>p) do   // 路径压缩
        begin
        temp:=data[d].Setfather;
        data[d].Setfather:=p;
        d:=temp;
        end;
Find:=p;
end;
PROCEDURE Link(p,q:Point); // p挂在q
begin
data[p].father:=q;
data[p].rightsub:=data[q].leftchild;
data[q].leftchild:=p;
data[q].degree:=data[p].degree+1;
end;
FUNCTION Merge(a,b:Point):point; // 按深度从小到大合并了两个队列返回合并后的根节点名称
var
        p                               :Point;
begin
if data[a].degree>=data[b].degree then
        begin
        p:=b;
        b:=data[b].rightsub;
        end else
        begin
        p:=a;
        a:=data[a].rightsub;
        end;
Merge:=p;
while (a<>0)and(b<>0) do
        begin
        if data[a].degree>=data[b].degree then
                begin
                data[p].rightsub:=b;
                p:=b;
                b:=data[b].rightsub;
                end else
                begin
                data[p].rightsub:=a;
                p:=a;
                a:=data[a].rightsub;
                end;
        end;
if a<>0 then data[p].rightsub:=a;
if b<>0 then data[p].rightsub:=b;
end;
FUNCTION Union(a,b:Point):Point; 合并两个二项堆名称为ab 返回合并后的名称
var
        h,p,prev,next                   :Point;
begin
h:=Merge(a,b);  // 合并队列
p:=h;          // 指针
prev:=0;       // p 的前导指针
next:=data[p].rightsub; // p 的后继指针
while next<>0 do   // 没停止
        begin
        if (data[p].degree<>data[next].degree) or   // 两树深度不同
                ((data[next].rightsub<>0)and(data[data[next].rightsub].degree=data[p].degree)) then // 三树深度相同我们要合并后两者所以掠过此点
                        begin
                        prev:=p;
                        p:=next;
                        end
                else if data[p].power>=data[next].power then  // 选择较小的p next挂在p
                        begin
                        data[p].rightsub:=data[next].rightsub;
                        link(next,p);
                        end
                else    // 选择较小的next p挂在next
                        begin
                        if prev=0 then h:=next
                                else data[prev].rightsub:=next;
                        link(p,next);
                        p:=next;
                        end;
        next:=data[p].rightsub;
        end;
Union:=h;
data[a].setfather:=h;
data[b].setfather:=h;
data[h].setfather:=0;    // 对集合的父亲做修改
end;
PROCEDURE Make(p:Point); // 调整二项树使之平衡
var
        temp                            :Longint;
        max,q,tp                        :Point;
begin
if p=0 then exit;
max:=-maxn;
tp:=data[p].leftchild;
q:=0;
while tp<>0 do
        begin
        if data[tp].power>max then
                begin
                max:=data[tp].power;
                q:=tp;
                end;
        tp:=data[tp].rightsub;
        end;
if (q<>0) and (data[p].power<data[q].power) then
        begin
        temp:=data[p].power;
        data[p].power:=data[q].power;
        data[q].power:=temp;
        Make(q);
        end;
end;
PROCEDURE Main;
var
        i,a,b,p,q,fa,fb,j              :Longint;
begin
readln(m);
for i:=1 to m do
        begin
        readln(a,b);
        fa:=find(a);
        fb:=find(b);
        if fa<>fb then // 两猴不认识
                begin
                p:=maxmember(fa);
                q:=maxmember(fb);
                data[p].power:=data[p].power div 2;
                data[q].power:=data[q].power div 2;
                make(p);
                make(q);
                writeln(data[maxmember(Union(fa,fb))].power);
                end else writeln(-1);
        end;
end;
BEGIN
step:=0;
while not eof do
        begin
        inc(step);
        MakeNull;
        Main;
        end;
END.
main(int argc, char* argv[])int N,M,x,y,rx,ry,hx,hxl,hxr,hy,hyl,hyr,i,nx,mx,j;while(scanf("%d",&N)!= EOF)for(i = 1;i <= N;i++)"%d",&H[i].val);"%d", &M);for(i=0;i<M;i++)"%d %d",&x,&y);if(rx == ry)"-1/n");continue;"%d/n",H[nx].val);return 0;
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值