首页 > 其他分享 >FHQ-Treap

FHQ-Treap

时间:2024-12-30 13:20:42浏览次数:1  
标签:return merge int pos son Treap bal FHQ

\(FHQ-Treap\) 是无旋平衡树的一种,码量相对少,并且简单易懂。

一下简称 \(treap\) (注意还有别的 \(treap\) ,但是在本文中仅指 \(FHQ-Treap\) )。

\(treap\) 仅需要合并和分裂。

\(treap\) 结合了小根堆(父亲节点权值比儿子小)和二叉查找树(左子树的值比根小,右子树的值比根大)的特性。

所以对于每个节点的值的大小,都满足左子树比他小,右子树比他大(或者一样大),而除了这个节点本身的值,我们还要给他赋一个随机权,作为小根堆的排名标准。

先来讲如何分裂。

定义结构体 \(bal\) ,\(t.a\)表示左孩子的节点编号,\(t.b\) 表示右孩子的节点编号,\(split\) 函数返回值类型为 \(bal\) ,参数是需要分裂的树的根的编号和需要按什么值 \(k\)分裂(即分裂后会返回两个根节点编号,一个树所有值小于 \(k\) ,另一个树所有值大于等于 \(k\) ),另外为了维护每个子树的大小,在分裂完之后一定要更新 \(siz\) 。

struct bal
{
	int a, b;
	bal(int aa = 0, int bb = 0)
	{
		a = aa; b = bb;
	}
};
void pushup(int x) { siz[x] = siz[son[x][0]] + siz[son[x][1]] + 1; }
bal split(int u, int x)
{
	if (!u) return bal(0, 0);
	if (key[u] < x)
	{
		bal t = split(son[u][1], x);
		son[u][1] = t.a;
		pushup(u);
		return bal(u, t.b);
	}
	else
	{
		bal t = split(son[u][0], x);
		son[u][0] = t.b;
		pushup(u);
		return bal(t.a, u);
	}
}

另外一个核心操作是 \(merge\) ,也就是分裂。函数返回值是合并后的根节点编号,参数是需要合并的两个子树的根节点的编号。对于这两个节点,一定是一个树上所有的值都小于另一个树上的值,不然无法同时维护随机权和值。如果第一个子树的随机权小于另一个(即更优先),将另一个子树和这个子树的右子树合并,否则反之。

int merge(int u, int v)
{
	if (!u || !v) return u + v;
	if (wei[u] < wei[v])
	{
		son[u][1] = merge(son[u][1], v);
		pushup(u);
		return u;
	}
	else
	{
		son[v][0] = merge(u, son[v][0]);
		pushup(v);
		return v;
	}
}

此外,以上两个操作都不要忘记判断边界情况。

接下来是插入某个值。

为了满足 \(merge\) 的两个子树的大小关系,我们不能将需要插入的值直接和原树合并,而是需要先把原树分裂成一个大于插入值,一个小于插入值,再依次合并,每次插入还要记得初始化。

void insert(int x)
{
	cnt ++; key[cnt] = x; wei[cnt] = rand1(), siz[cnt] = 1;
	bal t = split(root, x);
	root = merge(merge(t.a, cnt), t.b);
}

下面是删除某个数x。

我们希望有一个树的根的值是这个需要删除的值,然后直接合并他的左右儿子。我们还是考虑分裂,将原树分为 \(u\) 和 \(v\) 两个子树,其中 \(u\) 的所有值都小于 \(x\) ,\(v\) 中的所有值都大于等于 \(x\) ,但是此时我们不能直接删掉 \(v\) 的根节点,因为 \(v\) 的根节点不一定是 \(x\) ,所以还有一个办法:我们将 \(v\) 再分为两个子树,一个子树 \(a\) 的所有值都小于 \(x + 1\) ,另一个子树 \(b\) 的所有值都大于等于 \(x + 1\) ,这样子树 \(a\) 就只有值 \(x\) 了,放心删!

void erase(int x)
{
	bal wtz = split(root, x);
	bal gyx = split(wtz.b, x + 1);
	gyx.a = merge(son[gyx.a][0], son[gyx.a][1]);
	root = merge(wtz.a, merge(gyx.a, gyx.b));
}

接下来是根据值查找数 \(x\) 的排名。

这很简单,先把原树分为小于 \(x\) 的子树 \(u\) 和大于等于 \(x\) 的子树 \(v\),数 \(x\) 的排名就是 \(u\) 的大小加上 \(1\) 。

int find1(int x)
{
	bal wtz = split(root, x);
	int res = siz[wtz.a] + 1;
	root = merge(wtz.a, wtz.b);
	return res;
}

再来是根据排名 \(k\) 找值。

首先当前位置 \(pos\) 为 \(root\) (原树的根节点),如果 \(root\) 的左子树个数大于等于 \(k\) 说明这个值在左子树中,\(pos\) 更新为左儿子。

否则如果 \(pos\) 的左子树大小为 \(k-1\) ,即 \(pos\) 刚好为排名 \(k\) 的元素,就返回这个值。

如果都不是,说明这个值在右子树中,将 $pos $赋为右儿子, \(k\) 减去右儿子加上根的大小。

int find2(int x)
{
	int pos = root;
	while (1)
	{
		if (siz[son[pos][0]] + 1 == x) return key[pos];
		if (siz[son[pos][0]] >= x) pos = son[pos][0];
		else x -= siz[son[pos][0]] + 1, pos = son[pos][1];
	}
}

下面两个操作是查询前驱和后继。

其实只要理解了按照排名找值和按照值找排名,这两个操作会变得非常简单。

前驱就是排名比这个数的排名小 \(1\) 的数,后继就是比这个值大 \(1\) 的数的排名(注意平衡树中不一定存在这个大 \(1\) 的数,但是比他大 \(1\) 的数如果排个名,那么这个数的名次就是后继在原树中的名次)。

这两个操作都是先按值找排名,再按这个排名去找值就行了。

int lst(int x) { return find2(find1(x) - 1); }
int nxt(int x) { return find2(find1(x + 1)); }

这里补充一下随机权(即按照这个权值维护小根堆)的用处。

所谓平衡树,就是让这个树的高度是平衡的,因为合并和分裂是递归式的,所以如果树高为 \(n\) ,时间绝对爆炸,所以我们要用随机权平衡树高。虽然笔者能力有限,不会证明,但是结论是这样做可以使树高的期望维持在 \(log n\) 左右,那么总的时间复杂度就是 \(nlogn\)。

下面是模板题总代码。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n;
struct bal
{
	int a, b;
	bal(int aa = 0, int bb = 0)
	{
		a = aa; b = bb;
	}
};
int key[N], wei[N], son[N][2], cnt, seed = 1, siz[N], root;
int rand1() { return seed *= 19260817; }
void pushup(int x) { siz[x] = siz[son[x][0]] + siz[son[x][1]] + 1; }
bal split(int u, int x)
{
	if (!u) return bal(0, 0);
	if (key[u] < x)
	{
		bal t = split(son[u][1], x);
		son[u][1] = t.a;
		pushup(u);
		return bal(u, t.b);
	}
	else
	{
		bal t = split(son[u][0], x);
		son[u][0] = t.b;
		pushup(u);
		return bal(t.a, u);
	}
}
int merge(int u, int v)
{
	if (!u || !v) return u + v;
	if (wei[u] < wei[v])
	{
		son[u][1] = merge(son[u][1], v);
		pushup(u);
		return u;
	}
	else
	{
		son[v][0] = merge(u, son[v][0]);
		pushup(v);
		return v;
	}
}
void insert(int x)
{
	cnt ++; key[cnt] = x; wei[cnt] = rand1(), siz[cnt] = 1;
	bal t = split(root, x);
	root = merge(merge(t.a, cnt), t.b);
}
void erase(int x)
{
	bal wtz = split(root, x);
	bal gyx = split(wtz.b, x + 1);
	gyx.a = merge(son[gyx.a][0], son[gyx.a][1]);
	root = merge(wtz.a, merge(gyx.a, gyx.b));
}
int find1(int x)
{
	bal wtz = split(root, x);
	int res = siz[wtz.a] + 1;
	root = merge(wtz.a, wtz.b);
	return res;
}
int find2(int x)
{
	int pos = root;
	while (1)
	{
		if (siz[son[pos][0]] + 1 == x) return key[pos];
		if (siz[son[pos][0]] >= x) pos = son[pos][0];
		else x -= siz[son[pos][0]] + 1, pos = son[pos][1];
	}
}
int lst(int x) { return find2(find1(x) - 1); }
int nxt(int x) { return find2(find1(x + 1)); }
int main()
{
	cin >> n;
	int op, x;
	for (int i = 1; i <= n; i ++)
	{
		scanf("%d %d", &op, &x);
		if (op == 1) insert(x);
		else if (op == 2) erase(x);
		else if (op == 3) printf("%d\n", find1(x));//val->rank
		else if (op == 4) printf("%d\n", find2(x));//rank->val
		else if (op == 5) printf("%d\n", lst(x));
		else if (op == 6) printf("%d\n", nxt(x));
	}
	return 0;
}

参考文献 https://www.luogu.com.cn/article/vj041eul

标签:return,merge,int,pos,son,Treap,bal,FHQ
From: https://www.cnblogs.com/Lion-Wu/p/18640806

相关文章

  • 学习笔记:旋转treap
    前言更好的阅读体验。无旋treap。默认读者会BST的基本操作、堆和旋转。本文旋转部分和上面那篇文章的相同。代码中是小根堆。思想treap既是一棵二叉查找树(tree),也是一个二叉堆(heap)。但是如果这两个数据结构用同一个权值维护,那么这两种数据结构是矛盾的。所以treap用......
  • 浅析FHQ-treap
    前言更好的阅读体验默认读者会BST的基本操作。节点定义替罪羊树采用了懒惰删除的方法,不会立即删除某个点,而是在重构时不放进数组。structnode{intch[2],val;intsiz1,siz2,cnt,sum;//扣去懒惰删除的节点数量,没扣去懒惰删除的节点数量,树内相同权......
  • FHQ-treap 学习笔记
    FHQ-Treap学习笔记範浩強之木,無旋之奇構,併合眸,妙用無窮;其當官也,避繁複之旋。其視心有二,分若離,合若聚,若星漢分合變幻,肖無跡矣。不用旋,巧避繁,古之所未有,今之所獨異。茲樹形奇,如天成,真算之妙。---------《算枢奇构》###基本操作众所周知,无旋treap不需要旋转,基本操作有两个,分......
  • FHQ- Treap学习笔记
    FHQ-Treap与Treap都保证在第一关键字有序的情况下,维护第二关键字以达到平衡的目的。但是Treap用的是旋转,FHQ-Treap用的是分裂和合并。FHQ-Treap与Treap不同的地方:优美的分裂和合并。非旋。支持区间修改FHQ-Treap与Treap相同的地方:都保证在第一关键字有序的情况......
  • 笔记:FHQ Treap 之三分裂
    小知识点,但是好像没什么人写,所以写一篇。在NOIP之前积攒一点rp。需要的知识平衡树(FHQTreap)前言一般在写FHQTreap的时候,都是按照某个值或排名\(k\),将Treap分成小于等于和大于\(k\)的两棵树,我们将其称为二分裂。那么,所谓三分裂,就是将Treap按照小于、等于、大于......
  • FHQ-treap模板
    可以再加一个struct把整个树封装起来。。跟oiwiki学的#include<bits/stdc++.h>usingnamespacestd;#include<bits/stdc++.h>usingnamespacestd;structNode{Node*ch[2];intval,prio,cnt,siz;Node(int_val):val(_val),cnt(1),siz(1){......
  • Treap学习笔记
    Treap(树堆)学习笔记(此处为带旋Treap)Treap简介Treap是一种二叉搜索树,其中,权值val满足二叉搜索树的性质,节点优先级priority满足堆的性质(作用后面会讲到)Treap适用情况因为属于二叉搜索树,所以可以维护二叉搜索树的信息,带旋Treap可以更好地控制树的深度,使得每次操作不至于被......
  • 「FHQ-Treap —— 码量最小的平衡树」学习笔记
    不同于普通Treap,FHQ-Treap不需要左旋和右旋操作来处理数据。因此FHQ-Treap也称作无旋Treap。FHQ-Treap是基于Split(分裂)和Merge(合并)两种操作的平衡树。其与普通Treap的原理完全不同。一些基础的操作:例如Insert(插入元素)和Delete(删除元素)。对于Insert(插入元素),新建一......
  • 怎么优雅的踩爆 Treap
    在发布了文章Treap学习笔记后我认为我的平衡树能力已经登峰造极了。但是Treap真tmd太难写了,所以我们的czy大佬开发除了一种可以优雅的踩爆Treap的绝佳方案。#include<bits/stdc++.h>usingnamespacestd;intn;structtree{ vector<int>v; voidadd(intx){v.in......
  • FHQ_Treap
    先记个板子#include<bits/stdc++.h>usingnamespacestd;constintN=1e5+5;intn;intrt,son[N][2],sz[N],va[N],pri[N],tot;structFHQ{ voidpushup(intx){sz[x]=sz[son[x][0]]+sz[son[x][1]]+1;} intmerge(intx,inty) { if(!x||!y)returnx|y; if......