当前位置:首页 > 技术 > 正文内容

左偏树详解:合并与维护堆性质的数据结构

访客 技术 2026年6月17日 1

概述

左偏树(Leftist Tree)是一种可高效合并的堆式数据结构,支持在 $O(\log n)$ 时间内完成合并操作。它以二叉树形式组织,同时维护堆序性与特殊的"左偏"结构,适用于需要频繁合并堆的场景,如任务调度、优先队列合并等。

基本定义与约定

  • $val[u]$:节点 $u$ 的权值。
  • $lc[u], rc[u]$:节点 $u$ 的左、右子节点。
  • $dist[u]$:从节点 $u$ 到最近的"外节点"的距离。外节点指至少有一个子节点为空的节点。
  • $par[u]$:节点 $u$ 的父节点,用于并查集路径压缩优化。

本文默认使用小根堆,即父节点权值不大于子节点。

核心性质

1. 堆序性质

对任意非根节点 $u$,满足:

$$ val[par[u]] \leq val[u] $$

2. 左偏性质

对于任意节点 $u$,其左子树的最短路径不小于右子树:

$$ dist[lc[u]] \geq dist[rc[u]] $$

由此可得递推公式:

$$ dist[u] = dist[rc[u]] + 1 $$

这一性质保证了树的"右偏最小",从而确保合并操作的效率。

关键操作实现

合并(Merge)

合并是左偏树的核心操作。给定两棵左偏树根节点 $a$ 和 $b$:

  1. 若任一节点为空,返回另一节点。
  2. 确保 $val[a] \leq val[b]$,否则交换 $a$ 与 $b$。
  3. 将 $b$ 与 $a$ 的右子树递归合并。
  4. 合并后若左子树的 $dist$ 小于右子树,则交换左右子树以维持左偏性。
  5. 更新 $dist[a] = dist[rc[a]] + 1$。

int merge(int a, int b) {
    if (!a || !b) return a | b;
    if (val[b] < val[a]) swap(a, b);
    rc[a] = merge(rc[a], b);
    if (dist[lc[a]] < dist[rc[a]]) 
        swap(lc[a], rc[a]);
    dist[a] = dist[rc[a]] + 1;
    return a;
}

通过始终向右子树合并,并在破坏左偏性时调整,保证了结构平衡。

取最小值

最小值位于当前树的根节点。结合并查集进行路径压缩,使得后续查询能快速定位所在堆的根。


int findRoot(int x) {
    return par[x] == x ? x : par[x] = findRoot(par[x]);
}

删除根节点

删除根节点时,需将其左右子树重新合并为新堆,并更新父指针关系:


void deleteRoot(int &root) {
    int l = lc[root], r = rc[root];
    lc[root] = rc[root] = 0;
    dist[root] = 0;
    root = merge(l, r);
    par[l] = par[r] = root;
}

插入新节点

插入等价于将单节点堆与原堆合并:


root = merge(root, newNode);

懒标记下传(如整体加减)

支持不影响相对大小的操作(如整体加常数),可通过延迟标记实现。在每次访问子节点前下传标记:


void pushDown(int u) {
    if (tag[u]) {
        val[u] += tag[u];
        if (lc[u]) tag[lc[u]] += tag[u];
        if (rc[u]) tag[rc[u]] += tag[u];
        tag[u] = 0;
    }
}

注意:需在 merge、delete 等操作中调用 pushDown。

应用实例:猴子王国(Monkey King)

题目描述
初始有 $n$ 只猴子,每只独立成堆,权值为 $v_i$。进行 $m$ 次操作,每次指定两只猴子 $x,y$,将其所在堆的堆顶元素权值减半(向下取整),然后合并两个堆。输出每次合并后的堆顶值。

思路分析
- 使用左偏树维护每个猴群的优先级。 - 每次操作先找到 $x,y$ 所在堆的根。 - 修改根的权值为原值的一半(注意负数处理)。 - 分别删除旧根,将剩余部分重新合并,并把原根作为新节点插入。 - 最后合并两个堆并输出新堆顶。

技巧说明
由于 C++ 整除对负数向下取整,而题目要求正数下取整,故对奇数需特殊处理。例如,将 $a$ 减半应写作:


val = -(abs(val) >> 1); // 正确处理符号

参考实现


#include <bits/stdc++.h>
using namespace std;

const int MAXN = 3e5 + 5;
int n, m;

struct LeftistTree {
    int lc[MAXN], rc[MAXN], dist[MAXN], par[MAXN];
    struct Node {
        int id, val;
        bool operator < (const Node &b) const {
            return val != b.val ? val < b.val : id < b.id;
        }
    } node[MAXN];

    int find(int x) {
        return par[x] == x ? x : par[x] = find(par[x]);
    }

    int merge(int a, int b) {
        if (!a || !b) return a | b;
        if (node[b] < node[a]) swap(a, b);
        rc[a] = merge(rc[a], b);
        if (dist[lc[a]] < dist[rc[a]])
            swap(lc[a], rc[a]);
        dist[a] = dist[rc[a]] + 1;
        return a;
    }

    void updateHalf(int u) {
        node[u].val = -(abs(node[u].val) >> 1);
    }

    void delRoot(int &root) {
        int l = lc[root], r = rc[root];
        lc[root] = rc[root] = 0;
        dist[root] = 0;
        root = merge(l, r);
        if (root) par[root] = root;
    }
} T;

int main() {
    while (~scanf("%d", &n)) {
        for (int i = 1; i <= n; ++i) {
            T.node[i].id = i;
            T.node[i].val = -read(); // 转换为小根堆
            T.par[i] = i;
            T.lc[i] = T.rc[i] = 0;
            T.dist[i] = 1;
        }
        T.dist[0] = -1;

        m = read();
        while (m--) {
            int x = read(), y = read();
            int rx = T.find(x), ry = T.find(y);
            if (rx == ry) {
                puts("-1"); continue;
            }

            T.updateHalf(rx); T.updateHalf(ry);

            T.delRoot(rx); T.delRoot(ry);

            rx = T.merge(rx, T.par[rx]); // 重建堆
            ry = T.merge(ry, T.par[ry]);

            int newRoot = T.merge(rx, ry);
            T.par[newRoot] = newRoot;

            printf("%d\n", -T.node[newRoot].val);
        }
    }
    return 0;
}
标签: 左偏树

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。