猴王
时间限制: 1 Sec 内存限制: 128 MB
题目描述
很久很久以前,在一个广阔的森林里,住着n只好斗的猴子。起初,它们各干各的,互相之间也不了解。但是这并不能避免猴子们之间的争吵,当然,这只存在于两个陌生猴子之间。当两只猴子争论时,它们都会请自己最强壮的朋友来代表自己进行决斗。显然,决斗之后,这两只猴子以及它们的朋友就互相了解了,这些猴子之间将再也不会发生争论了,即使它们曾经发生过冲突。
假设每一只猴子都有一个强壮值,每次决斗后都会减少一半(比如10会变成5,5会变成2.5)。并且我们假设每只猴子都很了解自己。就是说,当它属于所有朋友中最强壮的一个时,它自己会站出来,走向决斗场。
输入
输入分为两部分。
第一部分,第一行有一个整数n(n<=100000),代表猴子总数。
接下来的n行,每行一个数表示每只猴子的强壮值(小于等于32768)。
第二部分,第一行有一个整数m(m<=100000),表示有m次冲突会发生。
接下来的m行,每行包含两个数x和y,代表第x个猴子和第y个猴子之间发生冲突。
输出
输出每次决斗后在它们所有朋友中的最大强壮值。数据保证所有猴子决斗前彼此不认识。
样例输入
5
20
16
10
10
4
4
2 3
3 4
3 5
1 5
样例输出
8
5
5
10
乍一看这道题,真的厉害。不打不相识得道理在这群猴子之中得到了完美得体现。那么问题来了,这道题和猴王有半毛钱的关系呀!!!!!
虽然与猴王没有任何关系,但是这题呢还是要做,这算法呢还是要讲。看到这道题的第一眼,我就觉得它不是什么好东西,结果我发现它果然不是什么好东西。这道题到底该怎么做呢?最容易想到的算法有几个,我在这里一个一个列出来并进行一波讲解。
1.堆
堆很容易想到,因为每次我们都是找猴子及其朋友中的最强者来打一架,所以一个大根堆,就可以解决问题。但是每次找两个猴子是不是朋友很麻烦,需要O(n)的时间进行遍历,这就衍生出了第二种算法。
2.堆+并查集
既然只用堆查找需要较长时间,那使用一个并查集来进行查找即可方便快速的查找,但是新的问题又出现了,如何把两个猴子的朋友的强壮值合并在一起呢?要知道合并两个大根堆绝非易事。码代码就可以码得人神魂颠倒。自然是很不科学。
3.二叉搜索树
二叉搜索树运用到这个题上也并非不可,因为二叉搜索树,本身建树的过程有一定的规律性,即大于根节点的往右走,小于根节点往左走,这就使得它可以更简单的合并,但是时间上还是伤不起。
4.终极算法——可并二叉堆(当然是可以用Fibonacci堆,但实在是太难写了,博主完全不会)
利用可并堆,对两个堆用O(log n)的时间去合并,用O(1)的时间进行查找,可以达到我们预想的效果。在下面会有详细的讲解。
可并堆 名字里面有堆,说明这还是一个堆,但是它拥有合并的功能。可并堆同时又叫做左偏树,说明他本身的形状很像一棵树,可以像树一样进行遍历与查找,并且左偏、左偏,这棵树以左儿子为重。所以我们建树的时候就要往左边偏,当然你要往右边偏也没人拦得了你。
这里说一下,左偏树的几个基本概念。
1.定义一棵左偏树中的外结点为左子树或右子树为空的结点为外结点。
2.定义结点i的距离dist[i]为结点i到它后代中最近的外结点所经过的边数。
3.左偏性质:一颗左偏树中任意结点的左子结点的距离不小于右子结点的距离
左偏树有几种基本操作:
1.合并
int Merge(int a,int b)
{
if(a==NULL) return b;
if(b==NULL) return a;
if(Tree[a].data<Tree[b].data) swap(a,b);
Tree[a].rchild=Merge(Tree[a].rchild,b);
if(Tree[a].rchild!=NULL) Tree[Tree[a].rchild].father=a;
if(Tree[a].lchild!=NULL) Tree[Tree[a].lchild].father=a;
if(Tree[Tree[a].lchild].dist<Tree[Tree[a].rchild].dist) swap(Tree[a].lchild,Tree[a].rchild);
if(Tree[a].rchild==NULL) Tree[a].dist=0;
else Tree[a].dist=Tree[Tree[a].rchild].dist+1;
return a;
}
/*dist表示距离,因其左偏性质,我们自然是要在右距离的值比左距离的值大的时候交换一下,由于我们是储存的时候只储存了编号,换一下值就会连带着一起转移掉,所以无所谓了。*/
2.删除
int delet(int p)
{
Tree[p].data/=2.0;
int l=Tree[p].lchild;
int r=Tree[p].rchild;
Tree[l].father=l;
Tree[r].father=r;
Tree[p].dist=Tree[p].lchild=Tree[p].rchild=NULL;
return Merge(l,r);
}
/*删除的时候就把根节点去掉,把它左儿子的父亲变为它自己,右儿子的父亲也变为它自己,合并左右两棵子树,再合并一下根节点。搞定!!!*/
下面放出猴王的代码:
//由于是道裸题就不在阐述太多,只有一些微弱的注释供诸君观赏
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;
const int maxm=200010;
struct node{
int lchild,rchild;
int dist,father;
int data;
node()
{
lchild=NULL;
rchild=NULL;
dist=0;
data=0;
father=NULL;
}
}Tree[maxm];//简单易懂的数组名
int n,m;
int inline readin() //蒟蒻的读入优化QAQ
{
int x=0;
char c=getchar();
while(c>'9'||c<'0') c=getchar();
while(c>='0'&&c<='9')
{
x=x*10+c-'0';
c=getchar();
}
return x;
}
int Merge(int a,int b)//合并函数
{
if(a==NULL) return b;
if(b==NULL) return a;
if(Tree[a].data<Tree[b].data) swap(a,b);
Tree[a].rchild=Merge(Tree[a].rchild,b);
if(Tree[a].rchild!=NULL) Tree[Tree[a].rchild].father=a;
if(Tree[a].lchild!=NULL) Tree[Tree[a].lchild].father=a;
if(Tree[Tree[a].lchild].dist<Tree[Tree[a].rchild].dist) swap(Tree[a].lchild,Tree[a].rchild);
if(Tree[a].rchild==NULL) Tree[a].dist=0;
else Tree[a].dist=Tree[Tree[a].rchild].dist+1;
return a;
}
int delet(int p)//删除函数
{
Tree[p].data/=2.0;
int l=Tree[p].lchild;
int r=Tree[p].rchild;
Tree[l].father=l;
Tree[r].father=r;
Tree[p].dist=Tree[p].lchild=Tree[p].rchild=NULL;
return Merge(l,r);
}
int search(int x)//其实可以用并查集,让程序跑得更快,但我懒得写了
{
if(Tree[x].father==x)
return x;
search(Tree[x].father);
}
void work()
{
int A,B,p,q;
for(int i=1;i<=m;i++)
{
int x,y;
x=readin(),y=readin();
p=search(x);
q=search(y);
A=delet(p);//A为删除后左右子树合并的根节点
B=delet(q);//B同A
A=Merge(A,p);//合并A与改变了值的根节点
B=Merge(B,q);//合并B与改变了值的根节点
A=Merge(A,B);//合并全新的A B
printf("%d\n",Tree[A].data);
}
}
int main()
{
n=readin();
for(int i=1;i<=n;i++)
{
Tree[i].data=readin();
Tree[i].father=i;
}
m=readin();
work();
}