清北学堂(2019 4 29 ) part 2

主要内容数据结构:

1.二叉搜索树

  一棵二叉树,对于包括根节点在内的节点,所有该节点左儿子比此节点小,所有该节点右儿子比该节点大,(感觉好像二分...)

  每个节点包含一个指向父亲的指针,和两个指向儿子的指针。如果没有则为空。每个节点还包含一个key值,代表他本身这个点的权值

常用操作:

插入一个数,删除一个数,询问最大/最小值,询问第k大值。

插入操作:

现在我们要插入一个权值为x的节点。
为了方便,我们插入的方式要能不改变之前整棵树的形态。
首先找到根,比较一下key[root]x,如果key[root] < x,节点应该插在root右侧,否则再左侧。看看root有没有右儿子,如果没有,那么直接把root的右儿子赋成x就完事了。
否则,为了不改变树的形态,我们要去右儿子所在的子树里继续这一操作,直到可以插入为止。

删除操作:

要删掉一个权值,首先要知道这个点在哪。

root开始,像是插入一样找权值为x的点在哪。

定义x的后继y,是x右子树中所有点里,权值最小的点。
找这个点可以x先走一次右儿子,再不停走左儿子。
如果yx的右儿子,那么直接把y的左儿子赋成原来x的左儿子,然后用y代替x的位置。

找第k大的数:

对每个节点在多记一个size[x]表示x这个节点子树里节点的个数。

从根开始,如果右子树的size k,就说明第k大值在右侧,往右边走。

如果右子树size + 1 = k,那么说明当前这个点就是第k大值。

否则,把k减去右子树size + 1,然后递归到左子树继续操作。

那么为什么呢?

证明:

  由二叉搜索树性质可得,左边字数一定比其父节点小,右边则反之

  看看正在处理的点的右子树根节点,对于其“size”,即节点个数,如果大于k,说明要找的树(或地址)肯定在右边

  因为左边的恒比右边小,如果此时size>=k,说明右面至少还有k个没找过的数,我们要找的是第k大的数,并不能允许右边有多于k个比目标大的数,那么向右找

  当找到当前节点的右子树根节点size+1=k时,说明这个数底下(包括自身)有k-1个比当前处理节点大的数,那么当前处理节点就是第k大的

  如果没等找到size+1=k就size<k了怎么办?

  说明右边比当前处理节点大的数并不足k,此时应向左找,找比当前节点小的数,也许时不时地向右子树找一下,最终找到第k大的数

  (我因为仔细读问题,没看见第k“大”的数...当第k个(小)的数证的,百思不得其解...)

2.二叉堆

一只神仙提前讲过,我之前也整过,然而用的优先队列,因为对就是棵完全二叉树,(看题目,“二叉”堆吖~)

一直没有手写堆,今天试试。

代码:

#include<bits/stdc++.h>        //本代码致力于维护小根堆
using namespace std;
int a[100005];
int n;
int size;
inline void up(int t){         //把数据上浮
    while(a[t]<a[t>>1]&&t){ swap(a[t],a[t>>1]); t>>=1; } } inline void down(int t){ //下沉 while((t<<1)<=size){ int l=t*2; int r=t*2+1; if(r>size){ //对于某些奇怪的只有左儿子的树的特判 if(a[t]>a[l]){ swap(a[t],a[l]); t=l; } break; } if(a[t]<=a[l]&&a[t]<=a[r])break; else if(a[l]<a[r]){ //判断把哪个儿子拿上来当爹 swap(a[t],a[l]); t=l; } else{ swap(a[t],a[r]); t=r; } } } inline void pop(){ //弹出堆顶 swap(a[1],a[size]); size--; down(1); } inline void add(int now){ //添加新元素 a[++size]=now; up(size); } int main(){ scanf("%d",&n); while(n){ n--; int x; scanf("%d",&x); add(x); } while(size){ printf("%d ",a[1]); pop(); } return 0; }
p.s.我为了确保这玩意对,特意又交了一遍快排模板

(我写这玩意用了一晚上(并不怕笑话...),老师说下课时我差点脏话出口...我发誓这种情况不多见)

3.区间RMQ问题

举个例子(例题):

给出一个序列,每次询问区间最大值.

看上去暴力模拟能过吖...

然而
N 100000, Q 1000000.

所以我们需要一套好算法,比如这个RMBRMQ,

主要思路是将总区间不断二分,根据这些区间进行计算,如图:

然后将所需访问区间拆分,但能保留的尽量大的区间一定要保留,因为大区间问问可以表示成许多小区间,只算大区间显然更快

延迟更新:

信息更新时,未必要真的做彻底的更新,可以只是将应该如何更新记录下来,等到真正需要查询准确信息时,才去更新 足以应付查询的部分。
在区间增加时,如果要加的区间正好覆盖一个节点,则增加其节 点的inc值和sum值,不再往下走.在区间询问时,还是采取正常的区间分解.
在上述两种操作中,如果我们到了区间[L, R]还要接着往下走,并且inc0,说明子区间的信息是不对的,我们将inc传送到左儿子和右儿子上,并将inc赋成0,即完成了一次更新.

延迟更新可避免每次都赋值的麻烦情况,可极大省时

4.并查集

等我做出来村村通再整...

先整下按秩合并:

对每个顶点,再多记录一个当前整个结构中最深的点到根的深度deepx.
注意到两个顶点合并时,如果把比较浅的点接到比较深的节点上.
如果两个点深度不同,那么新的深度是原来较深的一个.
只有当两个点深度相同时,新的深度是原来的深度+1.
注意到一个深度为x的顶点下面至少有2x个点,所以x至多为log N.
那么在暴力向上走的时候,要访问的节点至多只有log个 。

然而路径压缩更好...虽有两种算法一起用的sao操作,但为了避免一种“玄学错误”(???)而并不这么用(起码用的肥肠少...)

5.树及LCA问题

在一棵有根树中,树上两点x, yLCA指的是x, y向根方向遇到到第一个相同的点.
我们记每一个点到根的距离为deepx.
注意到x, y之间的路径长度就是deepx + deepy - 2 * deepLCA

两个点到根路径一定是前面一段不一样,(汇合)后面都一样.
注意到LCA的深度一定比x, y都要小.
利用deep,把比较深的点往父亲跳一格,直到x, y跳到同一个点上.
这样做复杂度是O(len).
首先不妨假设deepx < deepy.
为了后续处理起来方便,我们先把x跳到和y一样深度的地方.
如果xy已经相同了,就直接退出.
否则,由于xyLCA的距离相同,倒着枚举步长,如果x, y的第2j个父亲不同,就跳上去.这样,最后两个点都会跳到离LCA距离为1的地方,在跳一步就行了.
时间复杂度O(N log N). 

我..累了(QAQ)

转载于:https://www.cnblogs.com/648-233/p/10792824.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值