[可并堆] 左偏树

左偏树

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]棘手的操作。此题若能自己独立完成,那么你的左偏树就已经过关了。

posted @ 2022-11-27 15:18  PassName  阅读(47)  评论(0编辑  收藏  举报