左偏树
左偏树
左偏树解决的问题:高效合并两个堆。
例子:
你现在有两个大根堆,你要把这两个堆合并(合并完后还是大根堆)。
设当前两个堆的根为 \(rt1,rt2\) 。
假设 \(rt1\) 的值小于 \(rt2\) 的值。
那么根据大根堆的理论,\(rt1\) 应该是 \(rt2\) 的子孙。
那就出现了一个问题, \(rt1\) 是放左子树还是放右子树。
如果放左子树,那么就去合并 \(rt1\) 和 \(rt2\) 的左子树。
如果放右子树,那么就去合并 \(rt1\) 和 \(rt2\) 的右子树。
为了简化,那就索性做一个非常简单的决定:只放右子树。
设 \(f(x)=\) 从点 \(x\) 一直向右走能走的步数。
这样子合并的总次数就是 \(f(rt1)+f(rt2)\)。(两棵树都只会往右走,虽然中途有可能反复切换树根)

所以这样一来我们的目标就是最小化 \(f(rt1)+f(rt2)\),这就是左偏树的核心目标。
做法:
已知一个递推式 \(f(x)=f(rson)+1\),也就是说我们希望右子树的 \(f(rson)\) 尽量小。
那么若 \(f(lson)>f(rson)\) 显然交换左右子树更优.
于是我们就确定了左偏树最重要的性质: \(f(lson)\le f(rson)\)
时间复杂度:
设 \(n(d)\) 表示某个点的 \(f\) 值为 \(d\) 的子树的最少节点数。
有 \(n(0)=1\) 。
首先会发现 \(n(x)\) 这个函数是递增的,因为有一个式子是 \(n(x)\ge1+n(x-1)\)
那么对于一个节点 \(x\) , 假设其 \(f\) 值为 \(y\) ,那么右子树的 \(f\) 值必定为 \(y-1\) ,而左子树的 \(f\) 值大于等于 \(y-1\)
由于函数单调递增, 所以 \(n(y)\ge 2\times n(y-1)\)
故 \(f(x)\) 是 \(log\) 级别的。
那么一次修改就是 \(log\) 级别的。
左偏树实现细节:
-
\(merge\): 就如刚刚所说。
-
\(insert\): 把新加的节点当作一个左偏树,然后直接合并
-
\(pop\): 即删除根节点,直接合并根节点的左右子树
-
\(build\): 将节点放在队列里面,每次 \(merge\) 队首的两个树,然后把合并的结果丢到队尾。
-
\(top\): 直接查询顶值就行了
代码:
struct A{
int key,pos,dist;
int lson,rson;
}node[N];
int f[N];//并查集
int merge(int a,int b){//合并两棵树
if (!a) return b;
if (!b) return a;
if (check(a,b)){
swap(a,b);
}
node[a].rson=merge(node[a].rson,b);
int ls=node[a].lson,rs=node[a].rson;
if (node[ls].dist<node[rs].dist) swap(node[a].lson,node[a].rson);
node[a].dist=node[node[a].rson].dist+1;
return a;
}
bool vis[N];
int pop(int tp){//删除以tp为根的树
int ls=node[tp].lson,rs=node[tp].rson;
if (vis[tp]) return -1;//已经被删
int res=merge(ls,rs);
if (ls) f[ls]=res;
if (rs) f[rs]=res;
if (res) f[res]=res;
f[tp]=res;vis[tp]=1;
node[tp].lson=node[tp].rson=0;
return node[tp].key;
}
int main(){
//...
for (int i=1,opt;i<=m;i++){
cin >> opt;
if (opt==1){//合并
int l,r;
cin >> l >> r;
if (vis[l]||vis[r]) continue; //必要,如果任何一个已经被删除就直接跳过
int fal=getf(l),far=getf(r);
if (fal==far) continue;
int t=merge(fal,far);
f[fal]=f[far]=t;
f[t]=t;
}
else{//查询
int x;
cin >> x;
if (vis[x]) cout << -1 << endl;
else cout << pop(getf(x)) << endl;
}
}
return 0;
}

浙公网安备 33010602011771号