P3369 [テンプレート] 通常のバランスツリー (FHQ Treap ツリーの構築と分析)

トピックの説明

いくつかの数値を維持するには、データ構造 (トピックのタイトルを参照) を記述する必要があります。これには、次の操作が必要です。

  1. x 数値を挿入
  2. x 個の番号を削除します (同じ番号が複数ある場合は 1 つだけ削除します)
  3. x 数値のランキングをクエリします (ランキングは、現在の数値 +1 より小さい数値の数として定義されます)
  4. ランク x の数値をクエリする
  5. x の前任者を検索します (前任者は x 未満の最大の数値として定義されます)
  6. x の後続値を検索します (後続値は x より大きい最小の数値として定義されます)。

入力フォーマット

最初の行は n で演算の数を示し、続く n 行の各行には opt と x という 2 つの数字があり、 opt は演算のシーケンス番号を示します ( 1≤opt≤6 )

出力フォーマット

操作 3、4、5、6 の場合、各行は対応する答えを示す数値を出力します。

入力サンプルと出力サンプル

#1 を入力してコピーします

10 
1 106465 
4 1 
1 317721 
1 460929 
1 
644985 
1 84185 1 89851 
6 81968 
1 492737 
5 493598

#1 のコピーを出力します

106465 
84185 
492737

指示/ヒント

【データ範囲】
100%データの場合、1≦n≦105、∣x∣≦107

ツリーを構築します:

FHQTreap ツリーを構築します。

FHQ Treap ツリーの最終的な形式は、キーの値と優先順位によって決まります。その優れた点は、すべての操作で分割と結合の 2 つの基本操作のみが使用され、これら 2 つの操作の複雑さが O(log2 n) であることです。

前提知識:

  • C++
  • 二叉搜索树基本的な特性
  • 二叉堆

二分探索木では、左側のノードはルート ノードより小さく、ルート ノードは右側のノードより小さくなります。

インオーダートラバーサルを使用すると、単調増加シーケンスであることがわかります。

バイナリ ヒープ:

大きな根杭と小さな根杭があります。

大きなルート ヒープはルート ノード > 左ノードおよびルート ノード > 右ノードであり、小さなルート ヒープはその逆です。

大きなルートパイルと小さなルートパイルの上層は、通常、完全な二分木になります。このとき、検索の効率が向上し、時間の複雑さが軽減されます。O(log2n)

上記の知識ポイントを踏まえると、次のようになります。

トピックを分析します。

1. 二分木には左右のノードが必要です。

2. キー値、つまり確率変数が存在する必要があります。

3. 左右のノード+自身のサイズ(この時、ランキングに非常に役立ちます)

コードは以下のように表示されます:

struct Node {
	int ls, rs;   // 左右 子节点 
	int key, pri; //  key为值 pri 为随机的优先级
	int size; // 当前节点为根的子树的节点数量,用于求第k大和排名
}t[M]; 

芯:

1. L と R をルートとする 2 つのツリーを分割して返します

まず図を見てください。2 つのツリーに分割されており、左側のサブツリーは x より小さく、右側のサブツリーは x より大きくなっています。

 上記のコード:

void Split(int u, int x, int& L, int& R) {
	if (u == 0) { //到达叶子,递归返回
		L = 0, R = 0;
		return;
	}

	if (t[u].key <= x) { // 本节点比x小,那么到右子树找x 
		L = u;           // 左树的根是本节点  // 下一个如果到 这来 上一个的rs 为 这个节点u ,因为u.rs的全部字节点都大于 u的值
		Split(t[u].rs, x, t[u].rs, R); // 通过rs传回新的子节点  
	}
	else {
		R = u; // 根节点 
		Split(t[u].ls, x, L, t[u].ls);
	}
	Update(u);
}

2 つのツリーのルート L と R を返します。

2. 2 つのサブツリーをマージする

これは分割の逆であり、ランダムな値を使用してこれを行うと、木の高さが確実に低く保たれるため、ランダムな値が導入されます。

まず、小さなルート ヒープを例として、図に示すように 2 つのツリーをマージします。

 ここでは例としてビッグ ルート ヒープを使用します。

int Merge(int L, int R) {
	if (L == 0 || R == 0) {
		return L + R; // 左 or  右节点 
	} // 建立大顶堆
	if (t[L].pri > t[R].pri) { // 合并树  随机值 到的在右边
		t[L].rs = Merge(t[L].rs, R); // 
		Update(L);
		return L;
	}
	else {
		t[R].ls = Merge(L, t[R].ls);
		Update(R);
		return R;
	}
}

最も重いものが上になります。

二つの考えを持って書くほうがはるかに簡単です。

コードの説明についてはコメントを参照してください

問題解決コード:

#include<iostream>
#include<cmath>
#include<stdlib.h>
using namespace std;
const int M = 1e6 + 10;
int cnt = 0, root = 0;
struct Node {
	int ls, rs;   // 左右 子节点 
	int key, pri; //  key为值 pri 为随机的优先级
	int size; // 当前节点为根的子树的节点数量,用于求第k大和排名
}t[M]; 
void newNode(int x) {
	cnt++;
	t[cnt].size = 1;
	t[cnt].ls = t[cnt].rs = 0;
	t[cnt].key = x;
	t[cnt].pri = rand();
}
void Update(int u) {
	t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;
}
void Split(int u, int x, int& L, int& R) {
	if (u == 0) { //到达叶子,递归返回
		L = 0, R = 0;
		return;
	}

	if (t[u].key <= x) { // 本节点比x小,那么到右子树找x 
		L = u;           // 左树的根是本节点  // 下一个如果到 这来 上一个的rs 为 这个节点u ,因为u.rs的全部字节点都大于 u的值
		Split(t[u].rs, x, t[u].rs, R); // 通过rs传回新的子节点  
	}
	else {
		R = u; // 根节点 
		Split(t[u].ls, x, L, t[u].ls);
	}
	Update(u);
}

int Merge(int L, int R) {
	if (L == 0 || R == 0) {
		return L + R; // 左 or  右节点 
	} // 建立大顶堆
	if (t[L].pri > t[R].pri) { // 合并树  随机值 到的在右边
		t[L].rs = Merge(t[L].rs, R); // 
		Update(L);
		return L;
	}
	else {
		t[R].ls = Merge(L, t[R].ls);
		Update(R);
		return R;
	}
}

void Insert(int x) {
	int L, R; // 左右根的节点
	Split(root, x, L, R); 
	newNode(x); //生成x 
	int aa = Merge(L, cnt); //合并节点,这里是生成的节点,合并左子树中
	root = Merge(aa, R); //两棵树进行合并
}
void Del(int x) {//删除节点
	int L, R, p;
	Split(root, x, L, R); //先抛出 左根的节点 小于等于x   右根  大于 x
 	Split(L, x - 1, L, p); //在进行抛 右节点一定为 x 以p为根 ,左节点一定小于 x的,
	p = Merge(t[p].ls, t[p].rs); //我们只需将连接左右儿子,根节点就会被抛弃 
	root = Merge(Merge(L, p), R); // 合并左右子树
}

void Rank(int x) {//计算x的排名
	int L, R;
	Split(root, x - 1, L, R);
	printf("%d\n", t[L].size + 1); //左节点 + 1
	root = Merge(L, R);
}

int kth(int u, int k) { //计算排名为k的点 这个要和上面弄清楚
	if (k == t[t[u].ls].size + 1) {
		return u;
	}
	if (k <= t[t[u].ls].size) {
		return kth(t[u].ls, k);
	}
	if (k > t[t[u].ls].size) {  // 这里是 ls 不是 r 遍历右子树
		return kth(t[u].rs, k - t[t[u].ls].size - 1);
	}
}
void Precursor(int x) {
	int L, R;
	Split(root, x - 1, L, R);
	printf("%d\n", t[kth(L, t[L].size)].key);//这个size是节点的个数 刚好就是排名最后的点的位置
	root = Merge(L, R);
}
void Successor(int x) { //右子树的第一个点
	int L, R;
	Split(root, x, L, R);
	printf("%d\n", t[kth(R, 1)].key); // 和上面同理
	root = Merge(L, R);
}

int main() {
	srand(time(NULL));
	int n;
	cin >> n;
	while (n--) {
		int opt, x;
		cin >> opt >> x;
		switch (opt) {
		case 1:Insert(x); break;
		case 2:Del(x); break;
		case 3:Rank(x); break;
		case 4:printf("%d\n", t[kth(root, x)].key); break;
		case 5:Precursor(x); break;
		case 6:Successor(x); break;
		}
	}
	return 0;
}

おすすめ

転載: blog.csdn.net/zhi6fui/article/details/130108810