[lnsyoj118/luoguP3369]普通平衡树

news/2024/9/30 23:26:42

题意

维护一个数据结构,要求支持插入,删除,根据排名查数,根据数查排名,查询前驱,查询后继\(6\)个操作

sol

考虑到后四个查询的操作,会发现使用二叉搜索树(BST)完全可以实现
为了完成这四个操作,需要在每个节点记录\(3\)个值:

  1. \(key\) 表示当前节点的数
  2. \(cnt\) 表示当前节点的数的个数(为了防止出现同一数字出现多次)
  3. \(size\) 表示当前子树的数的个数(为了方便查询排名)

根据排名查数

当处于节点\(u\)时,设当前需要查询的排名为\(rank\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. 如果\(u.lson.size \ge rank\),说明此时要查询的数一定位于\(u\)的左子树,因此答案为左子树中排名为\(rank\)的数
  2. 如果\(u.lson.size + u.cnt \ge rank\),说明此时要查询的数为\(u.key\),因此答案就为\(u.key\)
  3. 前两条均不满足,则说明此时要查询的数一定位于\(u\)的右子树,又由于需要去除掉左子树和\(u\)的所有数,因此答案为右子树中排名为\(rank - u.lson.size - u.cnt\)的数

代码

int get_key(int u, int rank){if (!u) return INF;if (rank <= tr[tr[u].l].size) return get_key(tr[u].l, rank);if (rank <= tr[tr[u].l].size + tr[u].cnt) return tr[u].key;return get_key(tr[u].r, rank - tr[tr[u].l].size - tr[u].cnt);
}

根据数查排名

当处于节点\(u\)时,设当前需要查询的数为\(x\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. 如果\(x < u.key\),说明此时要查询的数一定位于\(u\)的左子树,因此答案为左子树中数\(x\)的排名
  2. 如果\(x = u.key\),说明此时要查询的数为\(u.key\),因此答案为\(u.lson.size + 1\)
  3. 前两条均不满足,则说明此时要查询的数一定位于\(u\)的右子树,又由于需要加上左子树和\(u\)的所有数,因此答案为右子树中数\(x\)的排名\(+u.lson.size + u.cnt\)

代码

int get_rank(int u, int key){if (!u) return 0;if (key < tr[u].key) return get_rank(tr[u].l, key);if (key == tr[u].key) return tr[tr[u].l].size + 1;return tr[tr[u].l].size + tr[u].cnt + get_rank(tr[u].r, key);
}

需要注意的是,部分时候为了方便,我们会在BST中加入两个哨兵节点\(-\infty\)\(+\infty\),此时由于\(-\infty\)的存在,根据排名查数时的\(rank\)需要\(+1\),而根据数查排名时的查得的答案需要\(-1\)

查询前驱

当处于节点\(u\)时,设当前需要查询的数为\(x\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. \(x \le u.key\),说明此时要查询的数一定不位于\(u\)的右子树,因此答案为左子树中数\(x\)的前驱
  2. \(x > u.key\),说明此时要查询的数可能为\(u.key\),也可能位于\(u\)的右子树,因此答案为右子树中数\(x\)的前驱与\(u.key\)中的最大值

代码

int get_prev(int u, int key){if (!u) return -INF;if (tr[u].key >= key) return get_prev(tr[u].l, key);return max(tr[u].key, get_prev(tr[u].r, key));
}

查询后继

当处于节点\(u\)时,设当前需要查询的数为\(x\),如果此时节点\(u\)为空节点,说明不存在该数,否则分情况讨论:

  1. \(x \ge u.key\),说明此时要查询的数一定不位于\(u\)的左子树,因此答案为右子树中数\(x\)的前驱
  2. \(x < u.key\),说明此时要查询的数可能为\(u.key\),也可能位于\(u\)的左子树,因此答案为左子树中数\(x\)的前驱与\(u.key\)中的最小值

代码

int get_next(int u, int key){if (!u) return INF;if (tr[u].key <= key) return get_next(tr[u].r, key);return min(tr[u].key, get_next(tr[u].l, key));
}

这样一来,这四个查询操作及两个修改操作的复杂度为\(O(h)\)\(h\)为BST高度。在随机数据下,\(h\)趋向于\(\log n\),但由于BST容易被卡的优秀性质,只需递增/递减数据就可以将BST卡成一条链,从而使\(h=n\),因此,我们需要一些手段来使BST的\(h\)无论何时都接近于\(\log n\),平衡树应运而生

旋转

BST有一条很好的性质:容易被卡中序遍历是单调递增的,反过来也成立,如果我们可以通过一些操作,使中序遍历不变,那么这棵树仍是本质相同的BST,而这个能够使中序遍历不变的操作即为旋转,旋转是几乎所有平衡树都需要使用的操作(部分除外,如FHQ-Treap
image
两图中序遍历都为\(A,Q,B,P,C\)
在执行\(zig\)操作时,需要进行三次改变:\(p.lson \to q.rson(B), q.rson \to p, p \to q\)
同理,在执行\(zag\)操作时,也需要进行三次改变:\(q.rson \to p.lson(B), p.lson \to q, q \to p\)
代码

void zig(int &u){int q = tr[u].l;tr[u].l = tr[q].r, tr[q].r = u, u = q;
}void zag(int &u){int q = tr[u].r;tr[u].r = tr[q].l, tr[q].l = u, u = q;
}

需要注意的是,这里的\(u\)指代的是根节点或某个节点的子节点,当执行\(zig\)\(zag\)时,所对应的节点也要改变,因此需要在函数中传递引用。旋转操作可以视为是BST上三条边所指节点的交换操作

Treap

Treap是OI中较常用的一种平衡树
Treap是Tree和Heap的结合体,它的原理非常简单粗暴:既然BST在随机数据下趋于\(\log n\),那么我们就把所有数据打乱顺序再插入就好了。显然,在\(99.99\%\)的情况之下,这种方法都是有效的。不过因为大多数平衡树解决的问题都是在线问题,因此我们无法简单地将数据打乱。
Treap给出的解决方案是这样的:对于每一个节点,在插入时赋予它一个随机权值\(val\),由于可以通过\(zig\)\(zag\)操作将BST的任一一对父子节点交换而不改变BST的本质,因此我们可以参考二叉堆,插入到对应位置后再向上调整,直到BST中的\(val\)仍然满足二叉堆的性质
对于插入操作,我们先将一个节点插入BST中,然后从下往上判断它是否需要调序;而对于删除操作,我们在BST中找到该节点后,为了方便操作,我们将该节点先调整到叶子结点上,再进行删除。具体代码见下:

void insert(int &u, int key){if (!u) u = create(key); // 没有该节点的话,就创建一个新节点else if (key == tr[u].key) tr[u].cnt ++ ; // 否则直接在节点上添加标记else if (key < tr[u].key){insert(tr[u].l, key);if (tr[tr[u].l].val < tr[u].val) zig(u); // 向上调序}else {insert(tr[u].r, key);if (tr[tr[u].r].val < tr[u].val) zag(u); // 向上调序}
}void erase(int &u, int key){if (!u) return ; // 没有该节点的话,无需处理else if (key == tr[u].key){if (tr[u].cnt > 1) tr[u].cnt -- ; // 如果存在多个标记,直接删除标记else if (tr[u].l || tr[u].r){if (!tr[u].r || tr[tr[u].l].val > tr[tr[u].r].val){zig(u); // 先向下调序erase(tr[u].r, key);}else{zag(u); // 先向下调序erase(tr[u].l, key);}}else u = 0; // 调到叶子节点后直接删除}else if (key < tr[u].key) erase(tr[u].l, key);else erase(tr[u].r, key);
}

需要注意的是,本题的\(size\)是会在旋转、插入、删除操作中随时改变的,类比线段树,我们还需要一个方法来根据子结点的数据反推节点的\(size\),即PUSHUP
代码:

void pushup(int u){tr[u].size = tr[tr[u].l].size + tr[tr[u].r].size + tr[u].cnt;
}

这样的话,我们就通过精巧的操作使BST基本平衡,平均时间复杂度也随之下降为\(O(n \log n)\),不过值得注意的是,其最坏复杂度仍为\(O(n^2)\),只是如果真的卡出来了,概率堪比十连十金

代码

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdlib>using namespace std;const int N = 100005, INF = 0x3f3f3f3f;struct Node{int l, r;int key, val;int cnt, size;
}tr[N];int root, idx;
int n;int create(int key){tr[ ++ idx].key = key;tr[idx].val = rand();tr[idx].cnt = tr[idx].size = 1;return idx;
}void pushup(int u){tr[u].size = tr[tr[u].l].size + tr[tr[u].r].size + tr[u].cnt;
}void zig(int &u){int q = tr[u].l;tr[u].l = tr[q].r, tr[q].r = u, u = q;pushup(tr[u].r);
}void zag(int &u){int q = tr[u].r;tr[u].r = tr[q].l, tr[q].l = u, u = q;pushup(tr[u].l);
}void build(){create(-INF), create(INF);root = 1, tr[1].r = 2;pushup(root);
}void insert(int &u, int key){if (!u) u = create(key);else if (key == tr[u].key) tr[u].cnt ++ ;else if (key < tr[u].key){insert(tr[u].l, key);if (tr[tr[u].l].val < tr[u].val) zig(u);}else {insert(tr[u].r, key);if (tr[tr[u].r].val < tr[u].val) zag(u);}pushup(u);
}void erase(int &u, int key){if (!u) return ;else if (key == tr[u].key){if (tr[u].cnt > 1) tr[u].cnt -- ;else if (tr[u].l || tr[u].r){if (!tr[u].r || tr[tr[u].l].val > tr[tr[u].r].val){zig(u);erase(tr[u].r, key);}else{zag(u);erase(tr[u].l, key);}}else u = 0;}else if (key < tr[u].key) erase(tr[u].l, key);else erase(tr[u].r, key);pushup(u);
}int get_rank(int u, int key){if (!u) return 0;if (key < tr[u].key) return get_rank(tr[u].l, key);if (key == tr[u].key) return tr[tr[u].l].size + 1;return tr[tr[u].l].size + tr[u].cnt + get_rank(tr[u].r, key);
}int get_key(int u, int rank){if (!u) return INF;if (rank <= tr[tr[u].l].size) return get_key(tr[u].l, rank);if (rank <= tr[tr[u].l].size + tr[u].cnt) return tr[u].key;return get_key(tr[u].r, rank - tr[tr[u].l].size - tr[u].cnt);
}int get_prev(int u, int key){if (!u) return -INF;if (tr[u].key >= key) return get_prev(tr[u].l, key);return max(tr[u].key, get_prev(tr[u].r, key));
}int get_next(int u, int key){if (!u) return INF;if (tr[u].key <= key) return get_next(tr[u].r, key);return min(tr[u].key, get_next(tr[u].l, key));
}int main(){scanf("%d", &n);build();while (n -- ){int op, x;scanf("%d%d", &op, &x);switch(op){case 1: insert(root, x); break;case 2: erase(root, x); break;case 3: printf("%d\n", get_rank(root, x) - 1); break;case 4: printf("%d\n", get_key(root, x + 1)); break;case 5: printf("%d\n", get_prev(root, x)); break;case 6: printf("%d\n", get_next(root, x)); break;default: break;}}return 0;
}

蒟蒻犯的若至错误

  • \(zig\)\(zag\)的时候没有PUSHUP导致整颗BST的\(size\)都计算错误

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

牛客周赛46(思路待补)

比赛链接:牛客周赛46赛时感受 本场参加的是内测,多亏了内测群的佬提供的思路,得以AK。 ABC都是简单的签到题,D稍微需要分类一下,EF有点算法知识,E可以使用前缀和+二分搜索过掉,但是听说好像还能使用离散化树状数组等等,F是数学知识,隔板法和求质数、求组合…

[TinyRenderer] Chapter1 p3 Line

(注:本小节不是对划线算法事无巨细的证明,如果你需要更加系统的学习,请跳转至文末的参考部分) 如果你是一名曾经学习过图形学基础的学生,那么你一定对画线算法稔熟于心,中点划线算法,Bresenham算法。其中,现代光栅化器中使用最多的就是Bresenham算法,它以去除了除法和…

3个月搞定计算机二级C语言!高效刷题系列进行中

前言 大家好,我是梁国庆。 计算机二级应该是每一位大学生的必修课,相信很多同学的大学flag中都会有它的身影。 我在大学里也不止一次的想要考计算机二级office,但由于种种原因,备考了几次都不了了之。 这一次我想换个目标! 备考计算机二级C语言 今天山东省考试院发布了关于…

讯飞有一个可以根据描述文本自动生成PPT的AI接口,有趣

文档:https://www.xfyun.cn/doc/spark/PPTGeneration.html 价格方面提供了免费1000点的额度,生成一次是10点,正好100次,如果要购买的话最低要购买1344元的,没有按量付费的模式,个人小开发者可买不起。 让我们跑起来玩玩,官方提供了python的sdk,下载到本地: 不想下载sd…

RSS 解析:全球内容分发的利器及使用技巧

RSS(Really Simple Syndication)是一种 XML 格式,用于网站内容的聚合和分发,让用户能快速浏览和跟踪更新。RSS 文档结构包括 `<channel>` 和 `<item>` 元素,允许内容创作者分享标题、链接和描述。通过 RSS,用户可以定制新闻源,过滤不相关信息,提高效率。RS…

2024.6.10漏洞探针

探针(扫描器) 1、nmap漏洞库,根目录下scripts中调用2、Goby(红队版) 直接输入ip扫描资产,漏洞库较少; 3、Nessus 本地安装: 下载安装普通版;注册获取验证码; 注册用户 nessus,nessus123 漏洞利用 1、工具框架 metasploit和searchsploit 忍者系统可以一键使用msf;2、…

6.12Web应用漏洞发现探针利用

已知CMS、开发框架、 思路: 各个页面查看数据包(地址信息),查看框架,上fofa关键字搜索(查看其框架信息如thinkhphp),利用检测工具测试漏洞情况; 网站根目录下的robots.txt文件信息; /data/admin/ver.txt 网站升级时间; 常见SQL注入、登录、逻辑越权支付逻辑绕过思路:…

6.13API接口服务类漏洞探针

ip地址解析:www.x.x.x.com, 对应网站目录为d:/wwwroot/xiaodi/ 而127.x.x.x,对应网站目录为d:/wwwroot/,可能存在网站备份文件zip,所以ip网址端口都的扫描; 协议端弱口令爆破: 超级弱口令检查工具;端口服务安全问题(用于无思路时) 思路:利用探针对端口探测后,对口令安全…