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

左偏树:可并堆的高效实现

访客 技术 2026年6月5日 1

左偏树是一种特殊的可并堆数据结构,它不仅具备堆的基本特性,还能高效地执行合并操作。本文将详细介绍左偏树的原理、性质及实现方法。

基础知识回顾

优先队列 在算法竞赛中,我们经常使用优先队列进行优化。例如,使用C++的STL库可以这样实现:

priority_queue<int, vector<pair<int,int> >, greater<pair<int,int> > > q;

这会创建一个按照距离值(first元素)从小到大排序的队列,其中距离值就是键值。

二叉堆 二叉堆是一种特殊的完全二叉树,每个节点存储一个元素(或权值)。堆的性质要求:对于任意节点,其权值不小于其子节点的权值(大根堆),反之则为小根堆。

左偏树的定义

左偏树是一种可并堆的实现形式,它是一棵二叉树,每个节点除了左右子树指针外,还包含两个重要属性:键值(key)和距离(dist)。键值用于节点间的比较。

距离的定义与性质:

  • 当一个节点的左子树或右子树为空时,该节点称为外节点(external node)
  • 节点i的距离(dist(i))是从i到其后代中最近外节点所经过的边数
  • 如果节点本身是外节点,则其距离为0
  • 空节点的距离定义为-1
  • 一棵左偏树的距离通常指其根节点的距离

左偏树示意图

外节点示意图

核心性质

val表示key,即键值

左偏性质: 每个节点的左儿子的dist都大于等于右儿子的dist。因此,左偏树每个节点的dist都等于其右儿子的dist加一。需要注意的是,dist并不是深度,一条向左的链也是合法的左偏树。

性质1: 满足堆的性质,对于任意节点p,val[p] ≥(或≤)val[l], val[r]。

性质2: dist[x] = dist[r] + 1,即任意节点的距离等于其右子树的距离加1。

重要定理

定理: 对于包含n个节点的左偏树,其最大距离不超过log(n+1)−1,即max{dist} ≤ log(n+1)-1。

证明:设max{dist}=k,则节点数最少的左偏树是一棵完全二叉树,此时节点数为2^(k+1)-1,因此n ≥ 2^(k+1)-1,移项得:k ≤ log(n+1)-1。

引理: 如果一棵左偏树的距离为k,则这颗左偏树最少包含2^(k+1)−1个节点。

证明:由于左偏树的左子树距离一定大于等于右子树的距离,且左偏树的距离等于右子树距离加一,那么当节点数最少时,任意节点的左子树大小等于右子树大小,满足这样性质的二叉树就是完全二叉树。

基本操作实现

合并(merge) 合并两个堆时,首先选取键值较小(或较大)的根作为新堆的根节点,然后将该根的左儿子作为新堆的左儿子,递归地合并其右儿子与另一个堆作为新堆的右儿子。为满足左偏性质,合并后若左儿子的dist小于右儿子的dist,则交换两个儿子。

代码实现

int unite(int x, int y) {
    if (!x || !y) return x + y; // 若有一个堆为空,返回另一个堆
    if (tree[x].key > tree[y].key || (tree[x].key == tree[y].key && x > y)) swap(x, y);
    // 满足堆性质,取较小值作为根
    tree[x].right = unite(tree[x].right, y);
    tree[tree[x].right].parent = x; // 更新父节点
    if (tree[tree[x].left].dist < tree[tree[x].right].dist) 
        swap(tree[x].left, tree[x].right); // 左偏性质
    tree[x].dist = tree[tree[x].right].dist + 1; // 更新距离
    return x;
}

主函数中:

int X = find(x), Y = find(y);
if (X != Y)
    tree[X].parent = tree[Y].parent = unite(X, Y);

插入节点 将待插入节点视为一个单节点堆,使用上述合并操作将其与目标堆合并即可。

代码实现

tree[x].parent = tree[y].parent = unite(x, y); // x是待插入节点,y是目标堆

删除根节点 将根节点的键值设为特殊值(表示已删除),然后合并其左右子树即可。

代码实现

void remove_root(int x) {
    tree[x].key = -1; // 标记为已删除
    tree[tree[x].left].parent = tree[x].left; // 更新子节点的父节点
    tree[tree[x].right].parent = tree[x].right;
    tree[x].parent = unite(tree[x].left, tree[x].right); // 合并子树
}

【模板】左偏树(可并堆)

分析

本题是左偏树的基本应用,主要使用合并和删除操作。实现小根堆功能。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
int n, m, op, x, y;
struct Node {
    int left, right, parent, dist, value;
} tree[MAXN];
int find(int x) { // 并查集查找操作
    if (tree[x].parent == x) return x;
    tree[x].parent = find(tree[x].parent);
    return tree[x].parent;
}
int unite(int x, int y) {
    if (!x || !y) return x + y;
    if (tree[x].value > tree[y].value || (tree[x].value == tree[y].value && x > y)) swap(x, y);
    tree[x].right = unite(tree[x].right, y);
    tree[tree[x].right].parent = x;
    if (tree[tree[x].left].dist < tree[tree[x].right].dist) 
        swap(tree[x].left, tree[x].right);
    tree[x].dist = tree[tree[x].right].dist + 1;
    return x;
}
void remove_root(int x) {
    tree[x].value = -1;
    tree[tree[x].left].parent = tree[x].left;
    tree[tree[x].right].parent = tree[x].right;
    tree[x].parent = unite(tree[x].left, tree[x].right);
}
int main() {
    cin >> n >> m;
    tree[0].dist = -1; // 初始化空节点的距离
    for (int i = 1; i <= n; i++) {
        cin >> tree[i].value;
        tree[i].parent = i; // 初始化每个节点为独立堆
    }
    for (int i = 1; i <= m; i++) {
        cin >> op >> x;
        if (op == 1) {
            cin >> y;
            if (tree[x].value == -1 || tree[y].value == -1) continue;
            int X = find(x), Y = find(y);
            if (X != Y)
                tree[X].parent = tree[Y].parent = unite(X, Y);
        } else {
            if (tree[x].value == -1) {
                puts("-1");
            } else {
                int X = find(x);
                printf("%d\n", tree[X].value);
                remove_root(X);
            }
        }
    }
    return 0;
}

罗马游戏

分析

简化题意:当命令为'M'时合并两个堆;当命令为'K'时删除分数最低元素,因此需要实现小根堆。

注意:若元素已被删除,应输出0,而不是-1。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000010;
int n, m, x, y;
struct Node {
    int left, right, parent, value, dist;
} tree[MAXN];
char op;
int unite(int x, int y) {
    if (!x || !y) return x + y;
    if (tree[x].value > tree[y].value || (tree[x].value == tree[y].value && x > y)) swap(x, y);
    tree[x].right = unite(tree[x].right, y);
    tree[tree[x].right].parent = x;
    if (tree[tree[x].left].dist < tree[tree[x].right].dist) 
        swap(tree[x].left, tree[x].right);
    tree[x].dist = tree[tree[x].right].dist + 1;
    return x;
}
int find(int x) {
    if (tree[x].parent == x) return x;
    tree[x].parent = find(tree[x].parent);
    return tree[x].parent;
}
void remove_root(int x) {
    tree[x].value = -1;
    tree[tree[x].left].parent = tree[x].left;
    tree[tree[x].right].parent = tree[x].right;
    tree[x].parent = unite(tree[x].left, tree[x].right);
}
int main() {
    scanf("%d", &n);
    tree[0].dist = -1;
    for (int i = 1; i <= n; i++)
        scanf("%d", &tree[i].value), tree[i].parent = i;
    scanf("%d", &m);
    for (int i = 1; i <= m; i++) {
        cin >> op; scanf("%d", &x);
        if (op == 'M') {
            scanf("%d", &y);
            if (tree[x].value == -1 || tree[y].value == -1) continue;
            int X = find(x), Y = find(y);
            if (X != Y)
                tree[X].parent = tree[Y].parent = unite(X, Y);
        } else {
            if (tree[x].value == -1) {
                puts("0");
            } else {
                int X = find(x);
                printf("%d\n", tree[X].value);
                remove_root(X);
            }
        }
    }
    return 0;
}

Monkey King

分析

本题是左偏树应用的经典例题。简化题意:使x和y的值减半后重新合并,并输出合并后堆的根。若两个猴子在同一个堆中,输出-1。与前面题目不同的是,本题需要实现大根堆,因为每次都是值最大的元素进行战斗。

因此,在合并时只需将比较条件改为:

if (tree[x].value < tree[y].value) swap(x, y);

注意:本题是多测数据,需要记得清空数组。

代码实现

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
int n, m, x, y;
struct Node {
    int left, right, parent, value, dist;
} tree[MAXN];
int find(int x) {
    if (tree[x].parent == x) return x;
    tree[x].parent = find(tree[x].parent);
    return tree[x].parent;
}
int unite(int x, int y) {
    if (!x || !y) return x + y;
    if (tree[x].value < tree[y].value) swap(x, y);
    tree[x].right = unite(tree[x].right, y);
    tree[tree[x].right].parent = x;
    if (tree[tree[x].left].dist < tree[tree[x].right].dist) 
        swap(tree[x].left, tree[x].right);
    tree[x].dist = tree[tree[x].right].dist + 1;
    return x;
}
int remove_root(int x) {
    tree[x].value >>= 1;
    int new_root = unite(tree[x].left, tree[x].right);
    tree[x].left = tree[x].right = 0;
    tree[x].dist = -1;
    return tree[x].parent = tree[new_root].parent = unite(new_root, x);
}
int main() {
    while (cin >> n) {
        memset(tree, 0, sizeof(tree));
        tree[0].dist = -1;
        for (int i = 1; i <= n; i++) {
            scanf("%d", &tree[i].value);
            tree[i].parent = i;
        }
        scanf("%d", &m);
        for (int i = 1; i <= m; i++) {
            scanf("%d%d", &x, &y);
            int X = find(x), Y = find(y);
            if (X == Y) {
                puts("-1");
            } else {
                int l = remove_root(X), r = remove_root(Y);
                tree[l].parent = tree[r].parent = unite(l, r);
                printf("%d\n", tree[tree[l].parent].value);
            }
        }
    }
    return 0;
}

左偏树的应用非常广泛,除了上述提到的操作外,还可以实现更多高级功能,如堆的分裂、查找第k大元素等。掌握左偏树的原理和实现,对于解决各类数据结构问题都具有重要意义。

相关文章

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...

发表评论

访客

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