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) = 1(2);
2 (10) = 10 (2);
4 (10) = 100(2);
8 (10) = 1000 (2);
……
在二进制定义中,有这样一件奇妙的事情:1+1 = 10; 10 + 10 = 100;……我们实质上是重述了二进制加法的定义。
我们顺次把二进制的数位从右到左依次编号为1,2,……,p,……我们发现在第p位的两个1和在p+1位的一个1是等价的。
了解了以上这些事,大家也就知道了什么是二项树。下面是明确的定义:
二项树 Bk的定义是递归的定义的有序树。它有一个最大深度 k和堆的定义一样,也和上面我们说的二进制中的p一样。Bk 由两个二项树Bk-1 构成。设这两个树分别为Bm,Bn,我们连接它们的方式就是Bm的左儿子这项的是Bn,Bn的右兄弟指向的是原Bm的左儿子。这样我们看两个Bk-1和一个Bk就是等价的了。
那什么优势二项堆呢?
考虑:3 (10)= 11(2) = 1 + 10 ; 5(10)= 101(2) = 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; 合并两个二项堆名称为a,b 返回合并后的名称
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;