平衡树学习笔记

C/C++学习笔记:算法知识之平衡树学习笔记,收藏一波吧! 平衡树存储: size就是节点的个数。 value是节点代表的权值。 权值相同的两个节点被视为一个,num记录折叠数量。 rand是随机数,用来维护平衡树。 son就是两个儿子。 平衡树size更新: 实际操作中,各个变量的值都是不断更新的,size也不例外。 函数体: 一个节点包括的节点个数用脚也能想到:左儿子size与右儿子size的和,加上这个节点折叠数量。 ... 阅读详情

平衡树学不会的,这辈子都学不会的

一、前置芝士

平衡树本质上看是一种 BST(二叉排序树)

而 BST 其实是一种非常容易理解的良心数据结构

BST 保证了这颗树的中序遍历是单调上升的

举个例子:

BST 支持插入、删除以及查询等多种操作,我们一一来讲述


  • 插入

插入很简单

我们知道二叉排序树有一个性质:对于每一个结点,设它的点权为 v a l val val,那么它的左子树中的每一个点权值必定小于 v a l val val,它的右子树中的每一个点权值必定大于 v a l val val

插入时我们对于当前访问到的点的权值分讨

  1. 如果这个点的权值等于插入点的权值,直接放在这个点上即可,有的题目会把计数器加 1

  2. 如果这个点的权值小于插入点的权值,跳右子树

  3. 如果这个点的权值大于插入点的权值,跳左子树

最后,如果访问到一个空点,那么就把空点变成这个插入点即可


  • 查询

为什么先讲查询呢?

其一是因为删除操作较为复杂

其二是因为删除操作将要涉及到一些新的概念,我们先在查询中阐述

首先有一种常见的查询——第 K 大数

我们仍然利用 BST 的性质,每访问到一个点,就对于子树的大小情况进行如下分讨:

  1. 如果左子树的大小加 1 刚好等于 K,那么访问到的点即为答案

  2. 如果左子树大小小于等于 K,跳左子树

  3. 如果左子树大小大于 K + 1,我们把 K 减去左子树的大小,再减去 1,随后跳右子树

为什么要减掉一定的大小呢?

因为第 K 大指的是以当前访问到的点为根的子树的第 K 大

如果访问左子树那没有关系

如果访问右子树,那么这个第 K 大,需要减去左子树的大小,再减去当前访问到的这个点(大小为 1),才能往右子树跳

然后是另外一种形式的查询——前驱和后继

在单调上升的有序数列中,一个数的前驱即为它的前一个数,后继即为它的后一个数

换句话说,一个数的前驱是小于这个数的最大数,后继是大于这个数的最小数

(如果您觉得上述定义有歧义,那么你可以把本文中出现的所有前驱和后继理解为直接前驱和直接后继)

查询就比较简单了,也是对于当前访问到的点的权值分讨,这里就不在阐述


  • 删除

前面说了,删除操作比较复杂

因为它需要考虑多种情况

下面我们一一来讨论:

  1. 叶子结点

最简单的情况,直接删除即可(具体实现会有多种方式,对应多种不同的效果)

  1. 仅有一个子树的结点

由于仅有一个子树,那么我们可以直接用它的唯一子树代替这个结点

代替说得更具体一点,就是把这个唯一子树的根节点放在被删除的结点的位置

举个例子,在上文给出的图中,2 号结点是满足条件的一个结点

假设我们现在要删除 2 号结点

那么它唯一子树的根节点即为 1 号结点,我们直接用 1 号结点替换掉 2 号结点即可

  1. 有两个子树的结点

有两个子树的结点,我们就不能直接用子树的根替换删除掉的结点了(因为有两个根)

还是拿上文给出的图举例:

假设我们现在要删掉 3 号结点

我们在序列中来看这个操作造成的影响:

原序列: 1 2 3 4 5 6

新序列: 1 2 4 5 6

可以发现,3 号结点的前驱和后继接在了一起

那么我们任取前驱和后继中的一个放在这个删除的结点位置即可

如果你选的这个点是一个叶子结点,直接放即可

如果不是,可以证明它的父亲一定是被删除的结点,直接类似第二种情况替换即可


然而,学会了这些操作,你的平衡树才刚刚入门

为什么???

(因为你学的东西是 BST,而这玩意叫平衡树)

平衡树,顾名思义,它会保证整颗树趋于相对平衡

那为什么 BST 就不平衡呢?

举个例子,如果你按照 1 2 3 4 5 6 7 8 的顺序插入,那么这颗树会退化成一条链

而前面三个操作的复杂度是 O ( h ) O(h) O(h) 的, h h h 为树的深度

也就是说,如果数据中出现了一些比较长的单调上升子段,那么树的高度会很高,操作的时间复杂度从会从最优的 O ( log ⁡ 2 n ) O(\log_2n) O(log2​n) 退化至 O ( n ) O(n) O(n)

为了解决这种情况,伟大的数据结构——平衡树应运而生

(注意:各类平衡树的以上三种操作都是大同小异的,区别在于平衡整颗树的方法)

二、常见的平衡树

1.   S p l a y 1.~Splay 1. Splay

S p l a y Splay Splay 又称伸展树,它的很多操作都基于伸展操作

而伸展操作又是基于旋转操作

旋转操作是平衡树的一种基本操作,用于调整一颗树至平衡状态,除了 S p l a y Splay Splay 外,下文将要介绍的 T r e a p Treap Treap 也会有这种操作

下面来介绍一下旋转操作:

旋转分为左旋和右旋,又称 zag 和 zig,但是它的本质都是一个结点往它的父结点旋转,区别仅在于被旋转的结点是左儿子还是右儿子,所以下文在讲解过程中不一定会作区分

放一张图:

在左边的树中,我们把 7 号结点旋转,即可得到右边的树

同理,在右边的树中,如果我们把 5 号结点旋转,即可得到左边的树

拿左边的树旋转到右边来举例,在一次旋转中,3、5、6、7 四个结点的父子关系会有所改变

它们分别对应:爷爷、爸爸、自己和儿子

其中儿子有一些特殊,因为儿子相对自己的方向和自己相对爸爸的方向是相反的

可以发现旋转之后,树的结构仍然保持 BST 的性质,这就是我们想要的效果

给出示例代码:

void Rotate(int u)
{
	int f = fa[u], g = fa[f], k = (son[f][1] == u);	//k = 0 表示左儿子,反之则表示右儿子
    son[g][son[g][1] == f] = u, fa[u] = g;
    son[f][k] = son[u][k ^ 1], fa[son[u][k ^ 1]] = f;
    son[u][k ^ 1] = f, fa[f] = u;
	return Pushup(f), Pushup(u); //要先上传点 f 的信息,再上传点 u 的信息,很多情况下不需要上传点 g 的信息(如果点 f 原本为根节点,那么点 g 甚至不存在)
}

中间的三个转换关系可以自己手推一下,如果弄懂了其实就会很简单

那么下面就进入到伸展操作:

伸展操作可以简单描述为把某个结点一直旋转到自己祖先的下方,成为这个祖先的儿子,如果祖先为 0 则表示把这个结点转到根结点上

所以,伸展操作实际上并不是所谓的平衡操作,而是一种重构树的方法,类似搜索引擎,把可能会访问到较多次的结点尽量靠近根,这样每次操作的均摊复杂度是 O ( log ⁡ 2 n ) O(\log_2n) O(log2​n)

你可以通过把一个结点不断往上旋转 ,然后就会被某些毒瘤数据给卡掉

为了避免这种情况产生,我们采用双旋的方法

下面给出伸展操作的代码:

void Splay(int u, int to = 0)
{
	while(fa[u] != to)
	{
    	int f = fa[u], g = fa[f];
        if(g != to) (son[f][1] == u) ^ (son[g][1] == f) ? Rotate(u) : Rotate(f);	//第一次旋转,如果点 u 和点 f 相对于自己父亲的方向相同,就把点 f 旋转上去,否则把点 u 旋转上去
        Rotate(u);	//第二次旋转,把点 u 旋转上去
    }
    if(!to) rt = u;	//特判点 u 旋转到根结点的情况
	return;
}

至于具体证明,有兴趣的读者可以参考 S p l a y Splay Splay 原论文,这里不再阐述 (其实是根本不会)

如果实在理解不了,你可以拿一条链,把链底转到链顶,看看单旋和双旋的区别

可以发现,单旋后的树还是一条链,不过双旋后整颗树的深度规模整整缩小了一半!

这就是双旋的优势所在,所以以后一定别用单旋!

下面给出 P3369 【模板】普通平衡树 的 S p l a y Splay Splay 写法:

#include<iostream>
#include<cstdio>
#include<cstdlib>

#define MAXN 500005
#define INF 0x7FFFFFFF

using namespace std;

int n, tot = 0, rt = 0;

struct Tree { int fa, son[2], val, si, cnt; } t[MAXN];

void Link(int x, int y, int k)
{
	t[x].son[k] = y;
	t[y].fa = x;
	return;
}

void Update(int x)
{
	t[x].si = t[x].cnt;
	if(t[x].son[0]) t[x].si += t[t[x].son[0]].si;
	if(t[x].son[1]) t[x].si += t[t[x].son[1]].si;
	return;
}

void Rotate(int x)
{
	int y = t[x].fa, z = t[y].fa, k = (t[y].son[1] == x);
	Link(z, x, (t[z].son[1] == y));
	Link(y, t[x].son[k ^ 1], k);
	Link(x, y, k ^ 1);
	Update(y); Update(x);
	return;
}

void Splay(int x, int Goal)
{
	while(t[x].fa != Goal)
	{
		int y = t[x].fa, z = t[y].fa;
		if(z != Goal) (t[z].son[1] == y) ^ (t[y].son[1] == x) ? Rotate(x) : Rotate(y);
		Rotate(x);
	}
	if(!Goal) rt = x;
	return;
}

void Insert(int x)
{
	int u = rt, f = 0;
	while(u && t[u].val != x)
	{
		f = u;
		u = t[u].son[x > t[u].val];
	}
	if(u) t[u].cnt++;
	else
	{
		u = ++tot;
		if(f) t[f].son[x > t[f].val] = u;
		t[u].fa = f;
		t[u].son[0] = t[u].son[1] = 0;
		t[u].val = x;
		t[u].cnt = t[u].si = 1;
	}
	Splay(u, 0);
	return;
}

void Find(int x)
{
	int u = rt;
	if(!u) return;
	while(t[u].son[x > t[u].val] && t[u].val != x) u = t[u].son[x > t[u].val];
	Splay(u, 0);
	return;
}

int Get(int x, int k)
{
	Find(x);
	int u = rt;
	if((k && t[u].val > x) || (!k && t[u].val < x)) return u;
	u = t[u].son[k];
	while(t[u].son[k ^ 1]) u = t[u].son[k ^ 1];
	return u;
}

void Delete(int x)
{
	int p = Get(x, 0), s = Get(x, 1);
	Splay(p, 0); Splay(s, p);
	int k = t[s].son[0];
	if(t[k].cnt > 1)
	{
		t[k].cnt--;
		Splay(k, 0);
	}
	else t[s].son[0] = 0;
}

int Get_Number(int x)
{
	int u = rt;
	if(t[u].si < x) return 0;
	while(true)
	{
		if(x > t[t[u].son[0]].si && x <= t[t[u].son[0]].si + t[u].cnt) return t[u].val;
		if(x <= t[t[u].son[0]].si) u = t[u].son[0];
		else
		{
			x -= t[t[u].son[0]].si + t[u].cnt;
			u = t[u].son[1];
		}
	}
}

int main()
{
	Insert(INF); Insert(-INF);
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
	{
		int tp, x;
		scanf("%d%d", &tp, &x);
		if(tp == 1) Insert(x);
		if(tp == 2) Delete(x);
		if(tp == 3) { Find(x); printf("%d\n", t[t[rt].son[0]].si); }
		if(tp == 4) printf("%d\n", Get_Number(x + 1));
		if(tp == 5) printf("%d\n", t[Get(x, 0)].val);
		if(tp == 6) printf("%d\n", t[Get(x, 1)].val);
	}
	return 0;
}

可以发现在插入和查询操作中都有伸展操作,这是为了使整颗树尽量平衡以保证时间复杂度,尽量不要少写也不要多写 (因为邪恶的出题人会想尽办法卡你)

2.   T r e a p 2.~Treap 2. Treap

T r e a p Treap Treap 实际上是 T r e e Tree Tree 和 H e a p Heap Heap 两个单词的结合

顾名思义,它在保证了 BST 性质的同时,也保证了堆的性质

而这个堆的比较键值是什么呢?

(不会是随机吧)

没错,就是随机数!这样它的期望复杂度仍是 O ( n log ⁡ 2 n ) O(n \log_2 n) O(nlog2​n) 的!

(所以你如果人品不好用这玩意会 T 掉)

在插入或者删除掉一个数的时候,我们同样需要用到 S p l a y Splay Splay 中讲到的旋转操作,使得整颗树满足堆的性质

具体代码就不给了 (因为人品不太好所以一直不敢写)

我们来看看这道题目:P3391 【模板】文艺平衡树

对于 S p l a y Splay Splay 来说,我们只要做两次伸展操作,就可以使某一个子树代表这个区间内的所有数了,然后我们就可以直接在这个子树的根上打一个翻转标记,类似线段树一样延迟下传即可

但是 T r e a p Treap Treap 在元素固定的时候是静态的,你破坏不了它的结构

所以 T r e a p Treap Treap 有一个劣势就是很难进行区间操作

但是,伟大的队爷 fhq 发明了一种可以支持区间操作的 Treap,并且可以省去繁琐的旋转操作!

这种 T r e a p Treap Treap 被称做 f h q _ T r e a p fhq\_Treap fhq_Treap,也叫做无旋 T r e a p Treap Treap

f h q _ T r e a p fhq\_Treap fhq_Treap 主要基于两种操作: S p l i t Split Split 和 M e r g e Merge Merge

其中的 M e r g e Merge Merge 并不仅仅只是单纯的两颗平衡树的合并

为什么?看完 S p l i t Split Split 你就明白了

S p l i t Split Split 操作是指按照某一个分界点把整颗树分成两个部分

分为两种,一种是权值分裂,另一种是排名分裂,这里给出后者的代码:

void Split(int now, int k, int &a, int &b)
{
	if(!now) a = b = 0;
    else
    {
    	if(si[ls[now]] < k) a = now, Split(rs[now], k - si[ls[now]] - 1, rs[now], b);	//把当前结点分到 a 中,跳向右子树
        else b = now, Split(ls[now], k, a, ls[now]);	//把当前结点分到 b 中,跳向左子树
	}
    return Pushup(now);
}

那么 M e r g e Merge Merge 操作实际上就是把分裂出来的两颗树合并:

int Merge(int a, int b)
{
	if(!a || !b) return (a + b);
    if(rd[a] < rd[b])	//比较随机权值
	{
    	rs[a] = Merge(rs[a], b);
        return Pushup(a), a;
    }
    else
    {
    	ls[b] = Merge(a, ls[b]);
        return Pushup(b), b;
	}
}

所以,使用 f h q _ T r e a p fhq\_Treap fhq_Treap,我们可以通过分裂操作,弄出一个表示操作区间的子树,然后打标记即可

代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>

#define MAXN 100005

using namespace std;

int n, m;

struct Node { int ls, rs, si, vl, rd, rv; };
struct Treap
{
	int rt, cnt;
	Node p[MAXN];

	inline void New(int v) { cnt++; p[cnt] = (Node){0, 0, 1, v, rand(), 0}; return; }
	inline void Pushdown(int u) { if(p[u].rv) { swap(p[u].ls, p[u].rs); p[p[u].ls].rv ^= 1; p[p[u].rs].rv ^= 1; p[u].rv ^= 1; } return; }
	inline void Update(int u) { p[u].si = p[p[u].ls].si + p[p[u].rs].si + 1; return; }

	void Split(int u, int k, int &l, int &r)
	{
		if(!u) return (l = r = 0, void());
		Pushdown(u);
		if(p[p[u].ls].si < k)
		{
			l = u;
			Split(p[u].rs, k - p[p[u].ls].si - 1, p[u].rs, r);
		}
		else
		{
			r = u;
			Split(p[u].ls, k, l, p[u].ls);
		}
		Update(u);
		return;
	}

	int Merge(int l, int r)
	{
		if(!l || !r) return (l + r);
		if(p[l].rd < p[r].rd)
		{
			Pushdown(l);
			p[l].rs = Merge(p[l].rs, r);
			Update(l);
			return l;
		}
		else
		{
			Pushdown(r);
			p[r].ls = Merge(l, p[r].ls);
			Update(r);
			return r;
		}
	}

	void Output(int u)
	{
		if(!u) return;
		Pushdown(u);
		Output(p[u].ls);
		printf("%d ", p[u].vl);
		Output(p[u].rs);
		return;
	}
} t;

int main()
{
	srand((unsigned)(time(NULL)));
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) { t.New(i); t.rt = t.Merge(t.rt, i); }
	for(int i = 1, l, r; i <= m; i++)
	{
		scanf("%d%d", &l, &r);
		int a, b, c;
		t.Split(t.rt, r, a, b);
		t.Split(a, l - 1, a, c);
		t.p[c].rv ^= 1;
		t.rt = t.Merge(t.Merge(a, c), b);
	}
	t.Output(t.rt);
	return 0;
}

当然 f h q _ T r e a p fhq\_Treap fhq_Treap 也存在一个缺点:会比单旋 T r e a p Treap Treap 稍微慢一些,但是它的小码量是无可替代的


以上两种是比较常见的平衡树,实际上有一种很好理解的平衡树叫替罪羊树,在我的 这篇文章 中有提到

还有一种十分高效的平衡树是红黑树,是 STL 中的内置操作树,不过码量太大,这里就不做介绍

当然还存在更多的平衡树,有兴趣的读者可以自行查找相关资料~

三、树套树

(这里特指线段树套平衡树)

我们来看一个问题:P3380 【模板】二逼平衡树(树套树)

如果把里面的所有区间都变成全局,那么这个题就是平衡树的基本操作

可是怎么处理这个区间呢?

这个时候我们就要用到树套树了

我们考虑建立一颗表示位置的线段树,在线段树的每个节点上再建一颗权值平衡树,表示这个节点所代表区间内所有点的权值

这样,我们在查找区间排名时,就能够先在线段树上找到对应的区间,再在这些区间上挂的平衡树中查询了

那么我们来讲一下这个题目:

首先树套树的建树应该是 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2​n)的,在线段树的基础上,合并两颗平衡树会带一个 log ⁡ \log log 的复杂度

下面分别讲每一个操作:

  1. 我们先在线段树上找到区间 [ l , r ] [l, r] [l,r],再在每个结点的平衡树上寻找比 k 小的数的个数,最后加上 1 就是答案,复杂度 O ( log ⁡ 2 2 n ) O(\log_2^2 n) O(log22​n)

  2. 显然我们很难直接找,那么基于第 1 个操作,我们可以二分一个答案,然后再查这个数的排名,复杂度 O ( log ⁡ 2 3 n ) O(\log_2^3 n) O(log23​n)

  3. 我们再线段树上找出所有包含这个位置的区间,把区间内该位置原来的数删掉之后再插入新数即可,复杂度 O ( log ⁡ 2 2 n ) O(\log_2^2 n) O(log22​n)

  4. 在线段树上找到区间后,在每颗平衡树上求一个前驱,求最大值即可

  5. 类似操作四,在线段树上找到区间后,在每颗平衡树上求一个后继,求最小值即可

由于笔者太菜,这道题的代码先咕了,如果哪一天调出来了会补上

好了好了,我不咕了

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cctype>
#include<cassert>
#include<vector>

#define MAXN 500005
#define INF 0x7FFFFFFF
#define MOD 998244353

#define up(i, l, r) for(register int i = (l); i <= (r); i = -~i)
#define down(i, l, r) for(register int i = (l); i >= (r); i--)

using namespace std;

namespace IO
{
	int f; char c;

	template<typename T> inline void _Read(T &v)
	{
		v = 0; f = 1; c = getchar();
		while(!isdigit(c)) { if(c == '-') f = -1; c = getchar(); }
		while(isdigit(c)) { v = (v << 3) + (v << 1) + (int)(c - '0'); c = getchar(); }
		v *= f;
		return;
	}

	template<typename T> inline void _Write(T k)
	{
		if(k < 0) { putchar('-'); k = -k; }
		if(k > 9) _Write(k / 10);
		putchar((char)(k % 10 + '0'));
		return;
	}

	inline int Read() { int v; _Read(v); return v; }
	inline void Write(int v, char ed = '\n') { return _Write(v), putchar(ed), void(); }
}

using IO :: Read;
using IO :: Write;

int n, m, a[MAXN];

struct fhq_Treap
{
	int cnt, ls[MAXN << 4], rs[MAXN << 4], rd[MAXN << 4], si[MAXN << 4], vl[MAXN << 4]; 

	inline int Newnode(int v) { ++cnt; return ls[cnt] = rs[cnt] = 0, rd[cnt] = 1LL * rand() * rand() % MOD, si[cnt] = 1, vl[cnt] = v, cnt; }

	inline void Pushup(int now) { return si[now] = si[ls[now]] + si[rs[now]] + 1, void(); }

	inline void Split_Value(int now, int k, int &a, int &b)
	{
		if(!now) return a = b = 0, void();
		if(vl[now] <= k) a = now, Split_Value(rs[now], k, rs[a], b);
		else b = now, Split_Value(ls[now], k, a, ls[b]);
		return Pushup(now);
	}

	inline void Split_Rank(int now, int k, int &a, int &b)
	{
		if(!now) return a = b = 0, void();
		if(si[ls[now]] < k)	a = now, Split_Rank(rs[now], k - si[ls[now]] - 1, rs[a], b);
		else b = now, Split_Rank(ls[now], k, a, ls[b]);
		return Pushup(now);
	}

	inline int Merge(int a, int b)
	{
		if(!a || !b) return a + b;
		if(rd[a] < rd[b]) { rs[a] = Merge(rs[a], b); return Pushup(a), a; }
		else { ls[b] = Merge(a, ls[b]); return Pushup(b), b; }
	}

	inline int Getrank(int now, int k)
	{
		int res = 0;
		while(now)
		{
			if(vl[now] < k) res += si[ls[now]] + 1, now = rs[now];
			else now = ls[now];
		}
		return res;
	}

	inline void Insert(int &now, int vl)
	{
		int L, R; Split_Value(now, vl, L, R);
		return now = Merge(Merge(L, Newnode(vl)), R), void();
	}

	inline void Delete(int &now, int vl)
	{
		int rk = Getrank(now, vl);
		int a, b, c, d;
		return Split_Rank(now, rk + 1, a, b), Split_Rank(a, rk, c, d), assert(si[d] == 1), now = Merge(c, b), void();
	}

	inline int Precursor(int now, int k)
	{
		int res = -INF;
		while(now)
		{
			if(vl[now] < k) res = max(res, vl[now]), now = rs[now];
			else now = ls[now];
		}
		return res;
	}

	inline int Successor(int now, int k)
	{
		int res = INF;
		while(now)
		{
			if(vl[now] > k) res = min(res, vl[now]), now = ls[now];
			else now = rs[now];
		}
		return res;
	}
} trp;

struct Segment_Tree
{
	#define ls (now << 1)
	#define rs (now << 1 | 1)
	#define mid ((l + r) >> 1)
	
	int rt[MAXN << 4];

	inline void Build(int now, int l, int r)
	{
		up(i, l, r) trp.Insert(rt[now], a[i]);
		if(l == r) return;
		return Build(ls, l, mid), Build(rs, mid + 1, r);
	}

	inline int Query_Range(int now, int l, int r, int L, int R, int k)
	{
		if(L <= l && r <= R) return trp.Getrank(rt[now], k);
		int res = 0;
		if(L <= mid) res += Query_Range(ls, l, mid, L, R, k);
		if(R > mid) res += Query_Range(rs, mid + 1, r, L, R, k);
		return res;
	}

	inline void Modify(int now, int l, int r, int pos, int k)
	{
		trp.Delete(rt[now], a[pos]), trp.Insert(rt[now], k);
		if(l == r) return;
		if(pos <= mid) Modify(ls, l, mid, pos, k);
		else Modify(rs, mid + 1, r, pos, k);
		return;
	}

	inline int Query_Precursor(int now, int l, int r, int L, int R, int k)
	{
		if(L <= l && r <= R) return trp.Precursor(rt[now], k);
		int res = -INF;
		if(L <= mid) res = max(res, Query_Precursor(ls, l, mid, L, R, k));
		if(R > mid) res = max(res, Query_Precursor(rs, mid + 1, r, L, R, k));
		return res;
	}

	inline int Query_Successor(int now, int l, int r, int L, int R, int k)
	{
		if(L <= l && r <= R) return trp.Successor(rt[now], k);
		int res = INF;
		if(L <= mid) res = min(res, Query_Successor(ls, l, mid, L, R, k));
		if(R > mid) res = min(res, Query_Successor(rs, mid + 1, r, L, R, k));
		return res;
	}

	#undef ls
	#undef rs
	#undef mid
} st;

int main()
{
	srand((unsigned)(time(NULL)));
	n = Read(), m = Read();
	up(i, 1, n) a[i] = Read();
	st.Build(1, 1, n);
	while(m--)
	{
		int op = Read();
		if(op == 1)
		{
			int L = Read(), R = Read(), k = Read(); 
			Write(st.Query_Range(1, 1, n, L, R, k) + 1);
			continue;
		}
		if(op == 2)
		{
			int L = Read(), R = Read(), k = Read();
			int l = 0, r = 1e8, ans = 0;
			while(l <= r)
			{
				int mid = (l + r) >> 1;
				int rk = st.Query_Range(1, 1, n, L, R, mid) + 1;
				if(rk <= k) ans = mid, l = mid + 1;
				if(rk > k) r = mid - 1;
			}
			Write(ans);
			continue;
		}	
		if(op == 3)
		{
			int pos = Read(), k = Read();
			st.Modify(1, 1, n, pos, k);
			a[pos] = k;
			continue;
		}
		if(op == 4)
		{
			int L = Read(), R = Read(), k = Read();
			Write(st.Query_Precursor(1, 1, n, L, R, k));
			continue;
		}
		if(op == 5)
		{
			int L = Read(), R = Read(), k = Read();
			Write(st.Query_Successor(1, 1, n, L, R, k));
		}
	}
	return 0;
}

E N D \mathit{END} END

线段树套平衡树 对于区间第K大询问 及修改操作 pascal 版 对于线段树的每一个节点 都是一颗平衡树 若不涉及修改 只需将平衡树改为线性表 然后二分 立即下载

相关推荐

黑群晖二合一已损毁_手动修复黑群晖已损毁磁盘空间

lz的用法是这样的,一个中古级mac mini 2011上装有vmware esxi 7.6, 然后上面挂载了lz的华芸Asustor Nas的iscsi空间,lz用的是basic volume,然后esxi上面有虚拟的黑群晖6.2.2-24922,黑群晖的docker里跑着不可描述软件,但是发现下载速度过快时,比如有好几个文件同时下载,总速度超过60MB/s以后storage manager里很...

weixin_39669629的博客 1万+

平衡树(第K大)

#include<cstdio> #include<iostream> #include<cstring> #include<algorithm> using namespace std; #define maxn 1000005 typedef struct sbtnod { int key,left,right,size; }sbtno...

qq_43624815的博客 449

链路聚合Eth-trunk以及华为交换机配置实例

链路聚合简介 以太网链路聚合Eth-Trunk简称链路聚合,它通过将多条以太网物理链路捆绑在一起成为一条逻辑链路,从而实现增加链路带宽的目的。同时,这些捆绑在一起的链路通过相互间的动态备份,可以有效地提高链路的可靠性。 目的: 随着网络规模不断扩大,用户对骨干链路的带宽和可靠性提出越来越高的要求。在传统技术中,常用更换高速率的接口板或更换支持高速率接口板的设备的方式来增加带宽,但这种方案需要付出高额的费用,而且不够灵活。 采用链路聚合技术可以在不进行硬件升级的条件下,通过将多个物理接口捆绑为一个逻辑接口

ChaseAug的博客 2万+

两个区间第K大问题

一:静态无修改     经典的方法就是使用划分树。。不过划分树写起来有点纠结。。。(时代的眼泪啊)     现在这个问题一般都是使用主席树来解决,其思想是这样的:对于原序列每一个前缀,都在其上建立一颗以值为轴的线段树(当然要离散化),保存区间内的值出现的次数,当查询一段区间最小值的时候,相当于在 右端点的线段树减去左端点的线段树 所得到的线段树上找第k个有值的点。。。(注意:权值线段树是可做差

zsl的学习博客 992

NXP KW47蓝牙MCU低功耗实测:从8μA睡眠到5年续航的优化实战

在物联网和嵌入式系统开发中,低功耗设计是决定电池供电设备续航能力的关键技术。其核心原理在于通过事件驱动、状态机管理和精细的电源域控制,使设备在空闲时进入深度睡眠,仅在需要时快速唤醒工作,从而将平均电流从毫安级降至微安级。这项技术的工程价值在于能够实现纽扣电池供电设备长达数年的使用寿命,广泛应用于智能家居传感器、可穿戴设备、资产追踪和工业监测等领域。以NXP KW47系列无线MCU为例,其集成的Arm Cortex-M33双核与专用射频单元,配合多级睡眠模式和高效DC-DC转换器,为超低功耗蓝牙应用提供了硬件

weixin_30162121的博客 256

ANSYS MAXWELL内置式永磁同步电机(IPMSM)转矩最大时输入电流相位的确定

1.在转子转速为零时看一个周期内的转矩变化

qq_41752507的博客 6516

POJ2985 The k-th Largest Group(平衡树查询第K大)

传送门 平衡树查询第k大的裸题,用并查集来维护一下哪些猫是一组的就可以了。查询第k大要维护size域,保存一个节点下面的字数的大小,更新的时候从下边更新。 #include<cstdio> #define MAXN 200005 struct SBT { int v, sz, ch[2], cnt; } t[MAXN]; int n, m, cnt,...

weixin_34167819的博客 199

STM32H750VBT6在Keil5下载报错?三步搞定Flash Download Failed Cortex-M7问题

本文针对STM32H750VBT6在Keil5开发环境中常见的“Flash Download Failed Cortex-M7”下载报错问题,提供了系统性的三步解决方案。核心在于理解该芯片128KB内部Flash的特殊性,并详细指导用户检查更新Device Pack、精确配置Flash下载算法地址(0x08000000, 0x20000)以及进行硬件连接验证,从而快速修复调试器与芯片间的Flash操作协议问题。

weixin_29274969的博客 480

【poj2104】区间第k大(版本1)

【POJ2104】区间第k大 Description 给定一个长度为n的序列,m个询问,每个询问的形式为:L,r,k表示在[L,r]间中的第k大元素。 Input 第1行:2个数,n,m表示序列的长度和询问的个数 第2行:n个数,表示n个数的大小 第3-m+2行:每行3个数,L,r,k表示询问在[L,r]区间内第k小的元素

袋鼠与蒟蒻 440

YOLOv8自定义训练:车牌识别全流程(从标注到推理,附可视化代码)

车牌检测全流程实践指南 摘要:本文详细介绍了基于YOLOv8的车牌检测完整实现方案。针对车牌检测中的三大难点(小目标、多角度、光照变化),提供了从数据准备到模型部署的全流程解决方案。重点包括:1)数据采集与标注规范;2)针对小目标的YOLOv8模型优化配置;3)K-means锚框聚类方法;4)训练参数调优策略。通过实际验证,该方法可获得95%以上的检测准确率,并附有完整的代码实现和可视化工具,便于开发者快速复现应用。 (字数:149字)

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 849

复现论文《孤岛微电网环境下基于物理模型引导的深度强化学习VSG频率控制方法》

本文提出了一种结合物理模型与深度强化学习的孤岛微电网VSG频率控制方法。通过建立VSG小信号模型并设计可变系数控制器,将物理知识融入深度强化学习框架,解决了高维参数和机组耦合带来的控制难题。复现代码实现了三个核心组件:VSGSmallSignalModel建模VSG动态特性,VariableCoefficientController实现参数自适应调节,PhysicsGuidedDRL整合DQN算法与物理模型引导的奖励机制。该方法通过模型驱动与数据驱动的优势互补,提升了频率控制性能,其中物理模型既作为先验知识

huanghm88的专栏 127

今天要来点自平衡的二叉搜索树吗——平衡树学习笔记

平衡树学习笔记 在学习平衡树之前,我们首先得弄懂什么是平衡树……——引言 提示:本篇约3000字,学习此内容可能需要2小时 复习部分: 先回忆一下二叉搜索树,当插入一个元素时,如果它比父节点大,就作为父节点的左儿子,否则作为父节点的右儿子。 下面这一坨就是一棵二叉搜索树 如图,当我们需要在搜索树中搜索某个元素时,只需要看它与左右儿子的大小关系,然后向左或者向右走就可以了。 二叉搜索树本来是一种很...

weixin_30726161的博客 141

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

[平衡树] 平衡树学习笔记

好久不见! 就差这个坑就补齐了! 估计高三也不会太更博客了,不知道是不是NOIP前的最后一篇? 在博客园的新博客欢迎关注! 发现自己平衡树学了跟没学一样,退役了简单学了一发…… 平衡树有很多,比如AVL树,红黑树(RBTree)什么的,AVL被魔改成SBT?RBTree是系统中的平衡树。但是不常用不介绍了。 平衡树的本质是一棵二叉搜索树(Binary Search Tree,即BST),

HeRaNO@HSAHRBNU 1264

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

平衡树 - FHQ 学习笔记

平衡树 - FHQ 学习笔记 主要参考万万没想到 的 FHQ-Treap学习笔记。 本片文章的姊妹篇:平衡树 - Splay 学习笔记。 感觉完全不会平衡树,又重新学习了一遍 FHQ,一口气把常见套路都学完了。 一、大致内容及分类 FHQ(???),全称非旋转 Treap,是一种可以用于维护按权值、排名分裂的数据结构。它相比与 Splay 虽然常数较大,但是实现起来代码难度相对容易,而且由于它非...

EricQian的博客 2925

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

后缀平衡树学习笔记

后缀平衡树简介 后缀平衡树是一种动态维护后缀排序的数据结构。 具体而言,它支持在串\(S\)的开头添加/删除一个字符。 前置知识-重量平衡树 重量平衡树保证操作影响的最大子树大小是最坏的或均摊的或期望的\(O(logn)\)。 不采用旋转机制的重量平衡树-替罪羊树 替罪羊树依赖于一种暴力重构的操作。 它规定了一个平衡因子\(\alpha\),需要保证对于每个节点有\(\alpha \times s...

weixin_30684743的博客 247

Treap平衡树学习笔记

Treap平衡树学习笔记 放在前面的话 人类的本质是复读机,Treap的本质是二叉搜索树 Treap杀手锏——自动平衡 为什么要平衡? 树上旋转!!! 自动平衡!!! 小结 Treap操作 定义Treap 旋转 插入 删除 维护子树大小 求排名为kk的元素 求元素xx的排名 求元素的前驱后继 你以为学完Treap了?不!还有FHQ Treap 无需旋转的Treap 超强核心Merge ~~另一核心Split~~万物皆基于Merge 分裂Split 插入Insert 删除Delete 小结×2 放在最后的话

Bell的博客 586

[学习笔记]后缀平衡树

后缀数组+平衡树=后缀平衡树 支持动态插入字符(只能往前插入),即插入一个后缀,维护所有后缀的排名 插入后缀找到位置?平衡树上二分 法一: 哈希+二分,太慢 法二: 第一个字符不同,已经可以比较,否则比较第二个字符开始的后缀,之前这两个后缀排名已经处理好了。直接比较排名即可。 查询任意位置的排名? 法一: 暴力这个点往上跳log次,找到rank 法二: 每个点打一个ta...

weixin_34226706的博客 263

[学习笔记] 平衡树——Treap

前置技能:平衡树前传:BST 终于学到我们喜闻乐见的平衡树啦! 所以我们这次讲的是平衡树中比较好写的\(Treap\). (以后会写splay的先埋个坑在这) 好了,进入正题. step 1 我们知道,BST虽然很方便, 但是,它很容易被卡成一条链. 因此,我们需要一个能够保持平衡的BST. 于是就有了我们众所周知的平衡树. 而平衡树保持平衡的方法,据本蒟蒻所知就是旋转节点. 通过旋转BST的节点...

weixin_30569033的博客 217

[总结] 后缀平衡树学习笔记

定义 后缀平衡树,就是动态的维护后缀数组,可以 \(O(\log n)\) 在末尾插入字符,\(O(\log n)\) 查询 \(rank,SA\)。但是由于是维护的后缀信息,所以插入只能在末尾插入字符(然后转化成在开头加一个字符),相当于添加一个后缀。 在线构造 方法一: 我们需要一种能比较两个后缀大小的方法,最简单就是二分+Hash,\(O(\log n)\) 实现,再加上平衡树插入复杂度,总...

weixin_30951231的博客 167
上一篇: KD - Tree 学习笔记
下一篇: 动态DP 学习笔记
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值