洛谷题单指南-字符串-P3369 【模板】普通平衡树

news/2025/1/13 3:17:18/文章来源:https://www.cnblogs.com/jcwy/p/18513114

原题链接:https://www.luogu.com.cn/problem/P3369

题意解读:平衡树的基本操作,模版题。

解题思路:

1、二叉搜索树-BST

二叉搜索树满足这样的性质:每一个节点的权值大于它的左儿子,小于它的右儿子。

对BST进行中序遍历,将得到一个从小到大的有序序列,因此BST是为了维护一个有序序列的动态添加、删除、查找。

随机情况下,对树进行插入、查找、删除等操作的时间复杂度都是O(logN),

但是如果插入顺序是一个已经有序的序列,将退化成一条链,时间复杂度变成O(N)。

2、平衡树

平衡树就是为了解决BST中高度不均衡导致时间复杂度上升的问题,

为了使某个节点左右子树高度尽可能差距小,需要进行两个重要的操作:左旋、右旋

左旋:将以E为根的子树左旋,先令S = E->right,再E->right = S->left,然后S->left = E

右旋:将以S为根的子树右旋,先令E = S->left,再S->left = E->right,然后E->right = S

平衡树的具体实现方式有多种,如AVL、红黑树、Treap、Splay、Trie等等,本文主要介绍最好写的两种:Trie、Treap。

3、用01-Trie平替平衡树

为什么01-Trie可以平替平衡树?

首先,01-Trie高度是固定的,显然满足平衡的特点。

其次,01-Trie也满足左子树对应的值更小,右子树对应的值更大,能够维护序列的有序性。

最后,01-Trie实现平衡树,需要记录一些额外的信息:每个结点所在子树一共有有多个元素

但是,01-Trie作为平衡树也有一些缺点,比如:

占用空间较大,每个整数都拆成二进制作为树的节点。

不能处理负数,但是可以加上一个较大的数将负数转正。

在数据量不太大的情况下,还是可以使用的。

Trie实现平衡树的基本操作:

本题元素大小|x|<=10^7

int trie[N * 26][2], idx表示Trie树,int siz[N * 26]记录每个节点所在子树的元素个数。

a、插入

void add(int val)
{int u = 0;for(int i = 25; i >= 0; i--){int v = val >> i & 1;if(!trie[u][v]) trie[u][v] = ++idx;u = trie[u][v];siz[u]++;}
}

b、删除

void del(int val)
{int u = 0;for(int i = 25; i >= 0; i--){int v = val >> i & 1;if(!trie[u][v]) return;u = trie[u][v];siz[u]--;}
}

c、查找小于x的元素个数

int get_less(int val)
{int res = 0;int u = 0;for(int i = 25; i >= 0; i--){int v = val >> i & 1;if(v == 1) res += siz[trie[u][0]]; //如果val在右子树,则左子树所有数都是小于val的,要累加u = trie[u][v];if(!u) break; //如果val不存在,到这里就可以结束}return res;
}

d、查找第k个数

int get_kth(int k)
{int res = 0;int u = 0;for(int i = 25; i >= 0; i--){if(siz[trie[u][0]] < k) //左子树数量不足k,在右子树找{k -= siz[trie[u][0]]; //k要减去左子树的数量u = trie[u][1];res = res * 2 + 1;} else {u = trie[u][0];res = res * 2;}if(!u) break;}return res - INF;
}

100分代码:

#include <bits/stdc++.h>
using namespace std;const int N = 100005, INF = 1e7;int trie[N * 26][2], idx;
int siz[N * 26]; //siz[i]表示节点i所在子树中数的个数,根节点不需要记录//插入元素到trie
void add(int val)
{int u = 0;for(int i = 25; i >= 0; i--){int v = val >> i & 1;if(!trie[u][v]) trie[u][v] = ++idx;u = trie[u][v];siz[u]++;}
}//从trie中删除元素
void del(int val)
{int u = 0;for(int i = 25; i >= 0; i--){int v = val >> i & 1;if(!trie[u][v]) return;u = trie[u][v];siz[u]--;}
}//获取小于val的元素数量
int get_less(int val)
{int res = 0;int u = 0;for(int i = 25; i >= 0; i--){int v = val >> i & 1;if(v == 1) res += siz[trie[u][0]];u = trie[u][v];if(!u) break; //如果val不存在,到这里就可以结束}return res;
}//获取排名第k的元素
int get_kth(int k)
{int res = 0;int u = 0;for(int i = 25; i >= 0; i--){if(siz[trie[u][0]] < k) //左子树数量不足k,在右子树找{k -= siz[trie[u][0]];u = trie[u][1];res = res * 2 + 1;} else {u = trie[u][0];res = res * 2;}if(!u) break;}return res - INF;
}int main()
{int n;cin >> n;int opt, x;while(n--){cin >> opt >> x;if(opt == 1) x += INF, add(x); //元素值+INF,使得必然为非负数,才能加入01trieelse if(opt == 2) x += INF, del(x);else if(opt == 3) x += INF, cout << get_less(x) + 1 << endl;else if(opt == 4) cout << get_kth(x) << endl;else if(opt == 5) x += INF, cout << get_kth(get_less(x)) << endl;else x += INF, cout << get_kth(get_less(x + 1) + 1) << endl;}return 0;
}

4、Treap平衡树

Treap是Tree+Heap,也就是树+堆,通过树来维护BST结构,通过堆的性质来保证尽可能平衡。

具体来说,树的节点定义为:

struct Node
{int l, r; //l左子树,r右子树int val, pri; //val是节点权值,pri是随机数用来维护堆的性质int siz, cnt; //siz是节点为根的子树大小,cnt是节点重复元素的个数
} tr[N];
int idx; //树节点编号
int root; //根节点

通过val来维护BST的性质,如果val严格有序将导致树退化成链,因此引入一个随机数pri,并强制父节点的pri大于子节点pri(大根堆性质),通过维护此性质即可保持树的平衡。

通过siz,cnt这些附加信息,就可以实现查元素排名、查第k个元素、找前驱、找后继等操作。

Treap维护树的平衡只需要在插入元素的时候判断,如果插入元素后,导致子节点的pri大于父节点的pri,则进行相应的旋转操作(左旋or右旋)。

100分代码:

#include <bits/stdc++.h>
using namespace std;const int N = 100005, INF = 1e8;struct Node
{int l, r; //l左子树,r右子树int val, pri; //val是节点权值,pri是随机数用来维护堆的性质int siz, cnt; //siz是节点为根的子树大小,cnt是节点重复元素的个数
} tr[N];
int idx; //树节点编号
int root; //根节点//生成一个新节点
int get_node(int val)
{tr[++idx].val = val;tr[idx].pri = rand(); //随机值,通过维护大根堆特性确保尽量平衡tr[idx].siz = tr[idx].cnt = 1;return idx;
}//计算子树siz
void pushup(int &p)
{tr[p].siz = tr[tr[p].l].siz + tr[tr[p].r].siz + tr[p].cnt;
}//右旋
void rotate_to_r(int &p)
{int t = tr[p].l; tr[p].l = tr[t].r;tr[t].r = p;p = t;pushup(tr[p].r);pushup(p);
}//左旋
void rotate_to_l(int &p)
{int t = tr[p].r;tr[p].r = tr[t].l;tr[t].l = p;p = t;pushup(tr[p].l);pushup(p);
}   //初始化树
void build_tree()
{   //树中添加两个初始节点:极大值和极小值,避免出现边界问题get_node(-INF); get_node(INF);root = 1;tr[root].r = 2;pushup(root);if(tr[1].pri < tr[2].pri) rotate_to_l(root);
}void insert(int &p, int val)
{if(!p) p = get_node(val);else if(tr[p].val == val) tr[p].cnt++;else if(tr[p].val > val) {insert(tr[p].l, val);if(tr[tr[p].l].pri > tr[p].pri) rotate_to_r(p); //插入左子树后对不满足堆性质进行调整}else{insert(tr[p].r, val);if(tr[tr[p].r].pri > tr[p].pri) rotate_to_l(p); //插入右子树后对不满足堆性质进行调整}pushup(p);
}void erase(int &p, int val)
{if(!p) return;else if(tr[p].val == val){if(tr[p].cnt > 1) tr[p].cnt--; //找到有多个,减一个else if(!tr[p].l && !tr[p].r) //叶子节点,直接删除{p = 0;}else if(!tr[p].r || tr[tr[p].l].pri > tr[tr[p].r].pri) { //如果只有左子树,或者左子树pri大于右子树,则右旋,然后去右子树删除rotate_to_r(p);erase(tr[p].r, val);}else if(!tr[p].l || tr[tr[p].r].pri > tr[tr[p].l].pri){ //如果只有右子树,或者右子树pri大于左子树,则左旋,然后去左子树删除rotate_to_l(p);erase(tr[p].l, val);}}else if(tr[p].val > val) erase(tr[p].l, val);else erase(tr[p].r, val);pushup(p);
}//查询比val小的数的个数,由于第一个节点是-INF,因此比val小的数的个数就是排名
int get_less(int p, int val)
{if(!p) return 0; else if(tr[p].val == val) return tr[tr[p].l].siz; //p就是val,则p左子树大小就是比val小的数的个数else if(tr[p].val > val) return get_less(tr[p].l, val); //到左子树找else if(tr[p].val < val) return tr[p].cnt + tr[tr[p].l].siz + get_less(tr[p].r, val); //到右子树找,p和p的左子树都比val小,要累加
}//查询第k个数
int get_kth(int p, int k)
{if(!p) return 0; //没有找到else if(tr[tr[p].l].siz >= k) return get_kth(tr[p].l, k); //到左子树找else if(tr[tr[p].l].siz + tr[p].cnt >= k) return tr[p].val; //p就是第k个else if(tr[tr[p].l].siz < k) return get_kth(tr[p].r, k - tr[tr[p].l].siz - tr[p].cnt); //到右子树找
}//查找val的前驱,比val小的最大数
int get_prev(int p, int val)
{if(!p) return -INF;else if(tr[p].val >= val) return get_prev(tr[p].l, val);else return max(tr[p].val, get_prev(tr[p].r, val));
}//查找val的后继,比val大的最小数
int get_next(int p, int val)
{if(!p) return INF;else if(tr[p].val <= val) return get_next(tr[p].r, val);else return min(tr[p].val, get_next(tr[p].l, val));
}int main()
{int n;cin >> n;int opt, x;build_tree();while(n--){cin >> opt >> x;if(opt == 1) insert(root, x);else if(opt == 2) erase(root, x);else if(opt == 3) cout << get_less(root, x) << endl;else if(opt == 4) cout << get_kth(root, x + 1) << endl;else if(opt == 5) cout << get_prev(root, x) << endl;else if(opt == 6) cout << get_next(root, x) << endl;}return 0;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/825248.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vscode调式LUA(EmmyLua)

安装EmmyLUA插件或者在github中下载 https://github.com/EmmyLua/VSCode-EmmyLua https://github.com/jiehuali/VSCode-EmmyLua.git 增加调试Launch.json 打开文件夹后会变成create a launcher.json,点击Run And Debug, 选择EmmyLua New Debugger(这个是作者推荐的,更稳定些…

TSP问题-分支限界法求解

此为课题组所指导本科生和低年级硕士生学习组合优化问题汇报 所用教材:北京大学屈婉玲教授《算法设计与分析》 课程资料:https://www.icourse163.org/course/PKU-1002525003 承诺不用于任何商业用途,仅用于学术交流和分享 更多内容请关注许志伟课题组官方中文主页:https://…

专有云是什么

专有云,或称私有云,是一种仅供特定组织或企业使用的云计算环境。本文将介绍以下几个方面:1、专有云的定义与特性;2、专有云与公有云的对比;3、专有云的应用场景;4、如何构建和管理专有云。在定义与特性部分,我们将详细探讨专有云如何通过提供独享的资源和高度定制的服务…

”回溯算法“框架及练习题

”回溯算法“框架及练习题@目录一、回溯算法是什么?二、框架如下:本人其他文章链接 一、回溯算法是什么? 结论:回溯 = 穷举 解决一个回溯问题,实际上就是一个决策树的遍历过程路径:就是已经做出的选择 选择列表:就是你当前可以做出的选择 结束条件:就是base case条件,…

Golang 开源库分享:faker - 随机生成有趣的假数据!

GitHub 仓库链接:https://github.com/bxcodec/faker 简介 在开发和测试过程中,我们经常需要各种各样的测试数据。如果手动去生成这些数据,不仅耗时,还容易出错。faker 是一个 Go 语言的假数据生成库,可以快速生成各种字段的随机数据。这个库可以帮我们轻松生成各种属性的假…

ShellScript

StorageSrvShelScript 编写添加用户的脚本,存储在/shells/userAdd.sh目录。 当有新员工入职时,管理员运行脚本为其创建公司账号。 自动分配客户端账号、公司邮箱、samba目录及权限、网站账号等。 以userAdd lifei的方式运行脚本,lifei为举例的员工姓名前提条件 完成了LDAP服…

资源利用率提高30%,揭秘华为云Serverless高效、高密度调度优化原理

本文介绍了华为云对调度优化这一业界难题的探索之路,创新性提出了基于JIAGU的高效的资源优化调度系统。Key TakeawaysUSENIX ATC(USENIX Annual Technical Conference) 是计算机系统领域国际顶级学术会议之一(CCF-A),在国际上享有极高的学术声誉,2024年录用率仅为15.8%。…

PostgreSQL技术大讲堂 - 第70讲:PG数据库数据加载调优案例

PostgreSQL技术大讲堂 - 第70讲,主题:postgresq数据库数据加载调优案例 讲课内容:1、数据库参数调整2、后台进程cpu绑定调整3、数据库并行操作调整数据加载是每个DBA经常需要完成的工作,如何让数据加载变得更快,本期视频跟大家一起分享调优带来的乐趣。主讲老师: CUUG数据…

网桥VXLAN服务

VXLAN 服务网桥VXLAN服务 在appsrv和storagesrv上搭建vxlan。需求如下, 安装实验网桥 新建vxlan隧道,网桥名称为 br-vxlan,网桥的出口为vxlan100,id 为100. appsrv的隧道地址为172.16.1.1/24,storagesrv的隧道地址为172.16.1.2/24. 测试网桥之间二层的联通性。AppSrv yum i…

微信支付商户系统Native支付

简易demo演示 demo演示点击体验### Native支付介绍 目前微信支付有以下几种场景 * JSAPi支付,适合微信公众号及微信小程序 * APP支付 * H5支付 * Native支付,适合PC网站页面支付 [微信支付商户平台](https://pay.weixin.qq.com/) [微信支付Native接口文档](https://pay.weixi…

centos7下redis安装

第1:下载地址 网页地址:https://redis.io/download 下载链接:http://download.redis.io/redis-stable.tar.gz 版本:Linux版,支持CentOS等其他Linux操作系统 备注:也可以直接通过Linux去下载Redis安装包 下载命令:wget http://download.redis.io/releases/redis-5.0.5.ta…

从0搭建 Spring Cloud Alibaba 基础工程框架搭建

整个项目结构:技术栈:spring cloud alibaba、MySQL8、Mybatis-Plus、Nacos、knife4j 接口文档、Lombok 一. 开发环境安装JDK17安装 MySQL安装二. 工程搭建 2.1 构建父子工程 2.1.1 创建父工程创建⼀个空的 Maven 项目, 删除所有代码, 只保留 pom.xml 目录结构: 图二完善父工程…