咕咕咕
好久没写博客了,之前坚持三天就鸽了证明自己一个月啥都没学,以后还是要写的。
以下内容参考了大佬的博客和luoguP3377的题解区,%%%。
堆,这个肯定都知道。“不就是优先队列吗”,本来一直保持着这样的想法,直到前几天帮室友验一道给数据结构基础课出的题时,突然发现自己连个堆都实现不来(这就是不听课的后果)有一说一真的菜b,于是为了偷懒(?学了一个神奇的数据结构——左偏树。
首先,我们知道堆的两个性质:
[1].堆是一棵完全二叉树
[2].堆上各结点的值从根往下具有单调性(此处指非严格单调性即可以相等,下同)
由此,我们可以得到一些结论:
[3].设结点数量为 n n n的堆的最大深度为 d d d,由[1]得 2 d ≤ 2 l o g 2 n + 1 2^d\le2^{log_2n+1} 2d≤2log2n+1,深度最大为 l o g 2 n + 1 log_2n+1 log2n+1
[4].由[2]&[3]得,要将根节点调整为最值,最多从根开始遍历一整条链。因此往堆中增删根结点,调整堆的时间复杂度是 O ( l o g n ) O(logn) O(logn)
所以,我们就获得了一个 O ( l o g n ) O(logn) O(logn)时间动态求最值的数据结构。
但是,更进一步,如果要求合并两个大顶堆,该怎么做呢?
这题我会暴力啊 显然,如果操作数和 n n n差不多的话,暴力就会导致 O ( n 2 l o g n ) O(n^2logn) O(n2logn)的整体复杂度,这时就应该换个数据结构了。
在说左偏树之前,我们需要定义一个变量 d i s i dis_i disi,其定义为 i i i结点和最近空结点的距离 − 1 -1 −1,当 i i i为空结点时 d i s i = − 1 dis_i=-1 disi=−1,正是由于 d i s dis dis的存在,我们才能保证左偏树的效率。
首先,仿照堆,我们也说说左偏树的性质:
[1].对于左偏树上的所有结点,都存在 d i s l s ≥ d i s r s dis_{ls}\ge dis_{rs} disls≥disrs,其中 l s , r s ls,rs ls,rs分别指当前点的左右子结点
[2].左偏树上各结点的值和 d i s dis dis从根往下具有单调性
类比上面对堆的描述,我们也可以得到一些结论:
[3].由[1]&[2]得,对于每一个结点总存在 d i s i = d i s r s + 1 dis_i=dis_{rs}+1 disi=disrs+1,其中 r s rs rs为 i i i的右孩子。
[4].设结点数量为 n n n的左偏树的**根节点的 d i s dis dis**为 d d d,由[1]&[3]得 2 d + 1 ≤ n + 1 2^{d+1}\le n+1 2d+1≤n+1, d d d最大为 l o g 2 ( n + 1 ) − 1 log_2(n+1)-1 log2(n+1)−1
在上面的性质和结论的基础上,我们就可以进行核心的合并操作了:
设有两棵根节点为最小值的左偏树 a a a和 b b b, v i v_i vi表示 i i i结点的值, r i r_i ri表示 i i i结点的右孩子,假设 v a ≤ v b v_a\le v_b va≤vb。
由[3]&[4],我们可以知道一棵 n n n个结点的左偏树最多有 l o g n logn logn条的从根一直往右的边(下称为右向边),从而可以在 O ( l o g n ) O(logn) O(logn)的时间内在右向边上找到一个 v a ≤ v i ≤ v b ≤ v r i v_a\le v_i\le v_b\le v_{r_i} va≤vi≤vb≤vri,所以可以不影响左偏性地将 b b b插入到 i i i和 r i r_i ri之间,随后从 b b b开始往 a a a更新 d i s dis dis,同时判断是否存在不满足[3]的情况,若存在则交换当前结点的左右子树。
于是就愉快地结束啦!什么?你问怎么删除堆顶?
没问题呀,左偏树的子树都具有左偏性质,去掉堆顶就相当于把原来的根的两棵子树重新合并,直接把之前的根重置后merge就好了。
(图片来源:洛谷)
那就趁热来一……道例题吧!
hdu1512 Monkey King
题意大致是,初始有 n n n个互不认识的猴子,每个猴子都有一个战斗力,他们会打 m m m场架,每场架的双方为 a a a和 b b b,双方会找各自的朋友中战斗力最高的那个(可能是他自己)来打架,之后双方打了架的猴子元气大伤战力减半,然后两方猴成为了朋友(不打不相识?,对于每个 a a a和 b b b,若双方已经为朋友则输出 − 1 -1 −1,否则输出打完架之后,这一帮猴子中最高的战斗力。
这就是左偏树模板题了(虽然二项堆/斐波那契堆/配对堆好像也能做,不过我太菜了还不会),初始有 n n n个大顶堆,每次打架取出两堆顶,减半后放入原堆并把两个堆合二为一并用并查集维护朋友关系。左偏树写起来就很简单,来回merge一下就ac啦,单组数据时间复杂度 O ( m l o g n ) O(mlogn) O(mlogn)。
AC代码:
#include
#define N 100010
using namespace std;
int n;
struct LeftTree
{
int d,v,l,r,f;//分别代表dis,value,leftson,rightson,father
}lt[N];
void init(int n)//多组数据初始化
{
for(int i=1;i<=n;i++)lt[i].f=i,lt[i].l=lt[i].r=lt[i].v=lt[i].d=0;
}
int find(int x)//并查集
{
return (x^lt[x].f)?lt[x].f=find(lt[x].f):x;
}
int merge(int x,int y,int rt=0)//返回新堆的根
{
if(!x||!y){lt[x+y].f=rt?rt:x+y;return x+y;}
if(lt[x].v<lt[y].v)swap(x,y);//大顶堆
lt[x].f=rt?rt:rt=x;//更新父结点(根)
lt[x].r=merge(lt[x].r,y,rt);//递归右子树
if(lt[lt[x].l].d<lt[lt[x].r].d)swap(lt[x].l,lt[x].r);//调整左右结点dis
lt[x].d=lt[lt[x].r].d+1;
return x;
}
int m,x,y;
int main()
{
ios::sync_with_stdio(false);
while(cin>>n)
{
init(n);
for(int i=1;i<=n;i++)cin>>lt[i].v;
cin>>m;
while(m--)
{
cin>>x>>y;
int a=find(x),b=find(y),c,d,e;
if(a==b)
{
cout<<-1<<"\n";
continue;
}
lt[a].v>>=1;lt[b].v>>=1;
//注意取出堆顶之后要将左右孩子置空,不然会出现两结点互为父亲的情况
c=merge(lt[a].l,lt[a].r);lt[a].l=lt[a].r=0;
d=merge(lt[b].l,lt[b].r);lt[b].l=lt[b].r=0;
a=merge(a,c);b=merge(b,d);//放回减半的结点
e=merge(a,b);//合并两棵树
cout<<lt[e].v<<'\n';
}
}
}
P.S:一直有个疑问,树上的“结点”和“节点”应该用哪个,还是说哪个都可以吗直接说node不香吗