左偏树
是一种可并堆,支持 \(O(\log n)\) 插入,删除,合并,\(O(1)\) 访问最小值。
堆是一棵树,其每个节点都有一个键值,且每个节点的键值都大于等于/小于等于其父亲的键值.
左偏树是一棵二叉树,
定义:
- 左偏树的外节点:有子节点为空的节点。
- \(dist_u\) :节点 \(u\) 到其子树内最近的外节点的距离(空节点为 \(-1\))。
\(dist\) 可以理解为与树高(到其子树内最远的外节点的距离)相对的概念。
左偏
左偏树具有左偏的性质,即:
- 对于每个节点,其左子节点的 \(dist\) 均大于等于右子节点。
这一性质保证了从任一节点出发,连续移向右子节点至边缘,经过的节点数不超过 \(\log(\text总节点数)\)(因为选择左子节点的路径长度不小于右子节点);同时也使 \(dist_u=dist_{rson[u]}+1\) 。
其维护只需在右偏时,交换左右节点即可。
基本操作
合并两个左偏树
以大根堆为例,将两个左偏树中根节点键值更大的作为结果的根节点和左子树,再将较小的和右子树递归合并作为右子树。递归过程中更新 \(dist\) 并维护左偏性质。
删除节点
将其左右子树合并,替代原节点的位置,从原位置向上更新 \(dist\) 。
插入节点
将待插入节点视为一棵左偏树进行合并。
总结
左偏树通过左偏性质保证合并的时间复杂度,基于合并实现插入,删除等操作。
实现
手写还是太难了,代码明明不长,却硬调一个上午。
枚举注意事项:
- 空节点一定要特判!不然就会遇到包括但不限于
WA,MLE,RE的情况。 - 实际应用时找根节点需要并查集,由于时间限制要路径压缩,导致被删除的节点也可能被指向,所以其
top应指向新的根节点(而非设为 自己\(/\)0\(/\)-2147483648)。 - 还是由于路径压缩,如果要找直接父节点需要另开一个
fa数组,更新除路径压缩外与top相同。 - 在删除节点时,应使被删除节点的子结点的上溯数组
fatop指向自己。 - 左偏树似乎不支持在 \(O(\log n)\) 时间内直接修改元素值,只能先删除,再插入。
以下是一个模板:
Code
merge: 合并
del: 删除顶节点
struct Node{
int lc,rc;
int val,dist;
};
Node lt[100010];
int top[100010];
int find(int x){
return x==top[x]?x:top[x]=find(top[x]);
}
int merge(int u,int v){
if(!u)return v;
if(!v)return u;
if(lt[u].val>lt[v].val)swap(u,v);
top[v]=u;
lt[u].rc=merge(lt[u].rc,v);
top[lt[u].rc]=u;
if(lt[lt[u].lc].dist<lt[lt[u].rc].dist)swap(lt[u].lc,lt[u].rc);
lt[u].dist=lt[lt[u].lc].dist+1;
return u;
}
int del(int u){
if(lt[u].dist)top[u]=merge(lt[u].lc,lt[u].rc),top[top[u]]=top[u];
lt[u].dist=-1;
return lt[u].val;
}
发现合并操作是可以递推实现的,于是:
Code
merge: 合并
del: 删除
struct Node{
int lc,rc;
int fa,val,dist;
};
Node lt[1000010];
int top[1000010];
int find(int x){
return x==top[x]?x:top[x]=find(top[x]);
}
int merge(int u,int v){
if(!(u&&v))return u|v;
if(lt[u].val>lt[v].val)swap(u,v);
top[v]=u;
while(v){
if(lt[lt[u].rc].val>lt[v].val)lt[v].fa=u,swap(v,lt[u].rc);
u=lt[u].rc;
}
while(u!=lt[u].fa){
if(lt[lt[lt[u].fa].lc].dist<lt[u].dist)swap(lt[lt[u].fa].lc,lt[lt[u].fa].rc);
lt[lt[u].fa].dist=lt[lt[u].rc].dist+1;
u=lt[u].fa;
}
return u;
}
int del(int u){
if(lt[u].lc||lt[u].rc){
if(lt[u].fa==u){
lt[lt[u].lc].fa=top[lt[u].lc]=lt[u].lc;
lt[lt[u].rc].fa=top[lt[u].rc]=lt[u].rc;
}else{
lt[lt[u].lc].fa=top[lt[u].lc]=lt[u].fa;
lt[lt[u].rc].fa=top[lt[u].rc]=lt[u].fa;
}
int st=merge(lt[u].lc,lt[u].rc);
if(lt[u].fa!=u){
if(lt[lt[u].fa].lc==u)lt[lt[u].fa].lc=st;
else lt[lt[u].fa].rc=st;
}
top[u]=st;
}
lt[u].dist=-1;
return lt[u].val;
}
时间上应该有优势,但是调试上....死循环似乎比无限递归难调的多。
$我们深知自己的平凡,而行,于此途中$
浙公网安备 33010602011771号