トピックの説明
いくつかの数値を維持するには、データ構造 (トピックのタイトルを参照) を記述する必要があります。これには、次の操作が必要です。
- x 数値を挿入
- x 個の番号を削除します (同じ番号が複数ある場合は 1 つだけ削除します)
- x 数値のランキングをクエリします (ランキングは、現在の数値 +1 より小さい数値の数として定義されます)
- ランク x の数値をクエリする
- x の前任者を検索します (前任者は x 未満の最大の数値として定義されます)
- 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;
}