左偏树
0x00 绪言
左偏树是一种比较神奇的数据结构,代码实现类似于线段树,但又是一种原理和线段树完全不一样的数据结构,如果读者打算阅读此博客,一定要读完,不要只看前半部分分,容易造成误导。
0x01 dist 的定义和性质
对于一棵二叉树,我们定义外节点为左儿子或右儿子为空的节点,定义一个外节点的 \(dist\) 为 \(1\),一个不是外节点的节点 \(dist\) 为其到子树中最近的外节点的距离加一。空节点的 \(dist\) 为 \(0\)。
0x02 左偏树的定义和性质
左偏树是一棵二叉树,它不仅具有堆的性质,并且左偏的:每个节点左儿子的 \(dist\) 都大于等于右儿子的 \(dist\)。并且是一种可合并堆。
因此,左偏树每个节点的 \(dist\) 都等于其右儿子的 \(dist\) 加一。
需要注意的是,\(dist\) 不是深度,左偏树的深度没有保证,一条向左的链也是左偏树。
0x03 左偏树的操作
定义一颗左偏树
左偏树大部分情况下需要维护的东西很多,见代码。
struct Node
{
int val, dist;
int s[2];//左右节点
int f;//father
int lazy1, lazy2.....//同线段树
} t[N];
合并
合并两个堆时,由于要满足堆性质,先取值较小(为了方便,本文讨论小根堆)的那个根作为合并后堆的根节点,然后将这个根的左儿子作为合并后堆的左儿子,递归地合并其右儿子与另一个堆,作为合并后的堆的右儿子。为了满足左偏性质,合并后若左儿子的 \(dist\) 小于右儿子的 \(dist\),就交换两个儿子。
但是好麻烦啊。。。。
所以,我们可以采用一个复杂度低且方便实现的方法,随机合并。
int merge(int x, int y)
{
if (!x || !y)
{
return x | y;
}
if (t[y].val < t[x].val)
{
std::swap(x, y);
}
if (rand() & 1)
{
std::swap(t[x].s[0], t[x].s[1]);
}
t[x].s[1] = merge(t[x].s[1], y);
return x;
}
PS:这种合并并不是万能的,后半部分会给出一种新的合并方式。
插入
单个节点也可以视为一个堆,合并即可。
删除根
合并根的左右儿子即可。
void erase(int x)
{
int ul = t[x].ch[0], ur = t[x].ch[1];
t[x].val = -1;//可加可不加,看你怎么实现
t[ul].f = 0;
t[ur].f = 0;
t[x].f = merge(ul, ur);
}
删除任意节点
先将左右儿子合并,然后自底向上更新 \(dist\) 、不满足左偏性质时交换左右儿子,当 \(dist\) 无需更新时结束递归:
int &rs(int x)
{
return t[x].s[t[t[x].s[1]].dist < t[t[x].s[0]].dist];
}
void push_up(int x)
{
if (!x)
{
return;
}
if (t[x].dist != t[rs(x)].dist + 1)
{
t[x].dist = t[rs(x)].dist + 1;
push_up(t[x].f);
}
}
int merge(int x, int y)
{
if (!x || !y)
{
return x | y;
}
if (t[x].val < t[y].val)
{
swap(x, y);
}
t[rs(x) = merge(rs(x), y)].f = x;
push_up(x);
return x;
}
细心的读者肯定发现了:诶?合并操作怎么不随机了??是的,一旦存在 push_up
就需要维护 dist
的信息,这个时候只能用以上代码,切记。
整个堆 加、减、乘 一个数
其实可以打标记且不改变相对大小的操作都可以。
在根打上标记,删除根/合并堆(访问儿子)时下传标记即可:
void push_down(int x)
{
......//因题而异
}
int pop(int x)
{
push_down(x);
return merge(t[x].ch[0], t[x].ch[1]);
}
0x04 模板题代码实现
#include <iostream>
#include <cstdio>
#include <algorithm>
#define rint register int
#define endl '\n'
const int N = 1e6 + 5;
int n, m, fa[N];
bool vis[N];
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
struct Node
{
int val, dist;
int s[2];
} t[N];
struct Left_Heap
{
int merge(int x, int y)
{
if (!x || !y)
{
return x | y;
}
if (t[y].val < t[x].val)
{
std::swap(x, y);
}
if (rand() & 1)
{
std::swap(t[x].s[0], t[x].s[1]);
}
t[x].s[1] = merge(t[x].s[1], y);
return x;
}
} heap;
int main()
{
scanf("%d%d", &n, &m);
for (rint i = 1; i <= n; i++)
{
scanf("%d", &t[i].val);
fa[i] = i;
}
while (m--)
{
int x, y, op;
scanf("%d", &op);
if (op == 1)
{
scanf("%d%d", &x, &y);
if (vis[x] || vis[y] || find(x) == find(y))
{
continue;
}
fa[find(x)] = fa[find(y)] = heap.merge(find(x), find(y));
}
if (op == 2)
{
scanf("%d", &x);
if (!vis[x])
{
x = find(x);
vis[x] = 1;
fa[x] = fa[t[x].s[0]] = fa[t[x].s[1]] = heap.merge(t[x].s[0], t[x].s[1]);
printf("%d\n", t[x].val);
}
else
{
puts("-1");
}
}
}
return 0;
}
0x05 后话
模板题是可以随机合并的,但不代表所有题目都可以,有兴趣的读者可以尝试 [SCOI2011]棘手的操作。此题若能自己独立完成,那么你的左偏树就已经过关了。
标签:return,int,合并,dist,节点,左偏 From: https://www.cnblogs.com/spaceswalker/p/16929747.html