コンピュータサイエンスにおいて、ツリーとランダム化二分探索木は、順序付けられたキーの動的なセットを維持し、キー間の二分探索を可能にする、密接に関連した2つの二分探索木データ構造です。キーの挿入と削除のシーケンスの後、木の形状はランダム二分木と同じ確率分布を持つ確率変数になります。特に、高い確率でその高さはキーの数の対数に比例するため、各検索、挿入、または削除操作の実行には対数時間かかります。

ツリーは、1989 年にRaimund SeidelとCecilia R. Aragonによって初めて記述されました。 [ 1 ] [ 2 ]その名前は、treeとheapの合成語です。これは、各キーに (ランダムに選択された) 数値優先度が割り当てられたデカルト木です。任意の二分探索木と同様に、ノードの順序走査は、キーのソート順と同じです。木の構造は、ヒープ順序付けされているという要件によって決定されます。つまり、葉以外のノードの優先度番号は、その子の優先度以上でなければなりません。したがって、より一般的なデカルト木と同様に、ルートノードは最大優先度のノードであり、その左および右のサブツリーは、そのノードの左および右へのソート順のサブシーケンスから同じ方法で形成されます。
トレアプを説明する同等の方法は、優先順位が最も高いノードをリバランスせずに二分探索木に挿入することによって形成される、というものです。したがって、優先順位が独立した乱数(2つのノードが同じ優先順位を持つ可能性が非常に低いことを保証できる、十分な大きさの優先順位の分布から得られる)である場合、トレアプの形状は、ランダムに選択された挿入順序でリバランスせずにノードを挿入することによって形成される探索木であるランダム二分探索木の形状と同じ確率分布を持ちます。ランダム二分探索木は高い確率で対数的な高さを持つことが知られているため、トレアプについても同様です。これは、クイックソートが期待値で実行されるという二分探索木の議論を反映しています。時間。二分探索木がソートの動的問題バージョンの解であるとすれば、Treapsは特に、優先順位がピボットの選択を導く動的クイックソートに対応します。
Aragon and Seidel also suggest assigning higher priorities to frequently accessed nodes, for instance by a process that, on each access, chooses a random number and replaces the priority of the node with that number if it is higher than the previous priority. This modification would cause the tree to lose its random shape; instead, frequently accessed nodes would be more likely to be near the root of the tree, causing searches for them to be faster.
Naor and Nissim[3] describe an application in maintaining authorization certificates in public-key cryptosystems.
Treaps support the following basic operations:
In addition to the single-element insert, delete and lookup operations, several fast "bulk" operations have been defined on treaps: union, intersection and set difference. These rely on two helper operations, split and join.

結合アルゴリズムは以下のとおりです。
function join(L, k, R) if prior(k, k(L)) and prior(k, k(R)) return Node(L, k, R) if prior(k(L), k(R)) return Node(left(L), k(L), join(right(L), k, R)) return Node(join(L, k, left(R)), k(R), right(R))

分割アルゴリズムは以下のとおりです。
function split(T, k) if (T = nil) return (nil, false, nil) (L, (m, c), R) = expose(T) (k = m)の場合、(L, true, R) を返します。 (k < m)の場合、 (L', b, R') = split(L, k) (k > m)の場合、(L', b, join(R', m, R)) を返します。 (L', b, R') = split(R, k) return (join(L, m, L'), b, R'))
集合AとBを表す2 つのトレアプt 1とt 2の和集合は、 A ∪ Bを表すトレアプtです。次の再帰アルゴリズムは、この和集合を計算します。
function union(t 1 , t 2 ): if t 1 = nil: return t 2 if t 2 = nil: return t 1 if priority(t 1 ) < priority(t 2 ): swap t 1 and t 2 t < , t > ← split t 2 on key(t 1 ) return join(union(left(t 1 ), t < ), key(t 1 ), union(right(t 1 ), t > ))
ここでは、split関数は2つのツリーを返すものと想定されています。1つは入力キーより小さいキーを保持するツリー、もう1つは入力キーより大きいキーを保持するツリーです。(このアルゴリズムは非破壊的ですが、インプレース破壊型のバージョンも存在します。)
交差のアルゴリズムも同様ですが、結合ヘルパールーチンが必要です。和集合、交差、差集合のそれぞれの計算量は、サイズがmとnのトリープに対してO ( m log ( n / m + 1))であり、m ≤ nです。さらに、和集合への再帰呼び出しは互いに独立しているため、並列に実行できます。[ 4 ]
Split と Union は Join を呼び出しますが、treaps のバランス基準を直接処理しません。このような実装は通常「結合ベース」の実装と呼ばれます。
キーのハッシュ値を優先順位として使用し、構造的に等しいノードを構築時にマージする場合、マージされた各ノードはキーセットの一意の表現となります。特定のキーセットを表すルートノードは同時に1つしか存在できないため、2つのキーセットはポインタ比較によって等価性をテストできます。この比較は時間的に一定です。
この手法を用いることで、2つのセット間の差が小さい場合でも、マージアルゴリズムの高速性を向上させることができます。入力セットが等しい場合、和集合関数と積集合関数はすぐに処理を中断し、いずれかの入力セットを結果として返しますが、差集合関数は空集合を返す必要があります。
dを対称差のサイズとする。修正されたマージアルゴリズムもO ( d log n / d )で制限される。[ 5 ] [ 6 ]
マルティネスとロウラがアラゴンとセイデルのツリーに関する研究に続いて導入したランダム化二分探索木[ 7 ]は、同じランダム分布の木の形状を持つ同じノードを格納しますが、ランダム化された構造を維持するために木のノード内に異なる情報を保持します。
ランダムな優先順位を各ノードに格納する代わりに、ランダム化二分探索木は各ノードに小さな整数、つまり子孫の数(自身を 1 と数える)を格納します。これらの数値は、回転ごとに一定の追加時間だけで、木の回転操作中に維持できます。既にn個のノードを持つ木にキーxを挿入する場合、挿入アルゴリズムは確率 1/( n + 1)でxを木の新しいルートとして配置することを選択し、そうでない場合は、挿入手順を再帰的に呼び出して、左または右のサブツリー内にxを挿入します(キーがルートより小さいか大きいかによって異なります)。子孫の数は、アルゴリズムによって各ステップでのランダムな選択に必要な確率を計算するために使用されます。サブツリーのルートにxを配置するには、treap のように葉に挿入してから上方向に回転させるか、Martínez と Roura によって記述された代替アルゴリズムを使用してサブツリーを 2 つの部分に分割し、新しいノードの左と右の子として使用するかのいずれかを実行できます。
ランダム化二分探索木の削除手順では、挿入手順と同じノードごとの情報を使用しますが、挿入手順とは異なり、削除されたノードの左と右の子から派生する2つのサブツリーを1つのツリーに結合するために必要なランダムな決定回数は平均でO(1)回だけです。これは、結合されるサブツリーの平均深さがΘ(log n)であるためです。サイズがnとmの2つのツリーを結合するには、平均でΘ(log(n+m))回のランダムな選択が必要です。削除されるノードの左または右のサブツリーが空の場合、結合操作は自明です。そうでない場合は、削除されたノードの左または右の子が、その子孫の数に比例する確率で新しいサブツリーのルートとして選択され、結合は再帰的に進行します。
ランダム化二分木では、ノードごとに格納される情報はトレアプよりも単純ですが(高精度の乱数ではなく小さな整数)、乱数生成器への呼び出し回数は多く(挿入または削除ごとに O(log n ) 回の呼び出し、挿入ごとに 1 回の呼び出し)、ノードごとの子孫数を更新する必要があるため、挿入手順はやや複雑になります。技術的な違いとしては、トレアプでは衝突(2 つのキーが同じ優先順位になる)が発生する可能性がわずかにあります。また、どちらの場合も、真の乱数生成器と、デジタル コンピュータで一般的に使用される擬似乱数生成器との間には統計的な違いが生じます。しかし、いずれの場合も、アルゴリズムの設計に使用される完全なランダム選択の理論モデルと実際の乱数生成器の機能との差は極めて小さいです。
ツリーとランダム化二分探索木はどちらも、更新ごとにツリーの形状がランダムに分布するという点では同じですが、挿入と削除操作のシーケンスでこれら2つのデータ構造によって実行されるツリーの変更履歴は異なる場合があります。たとえば、ツリーでは、1、2、3の3つの数値が1、3、2の順に挿入され、その後数値2が削除された場合、残りの2つのノードは、中央の数値が挿入される前と同じ親子関係を持ちます。ランダム化二分探索木では、削除後のツリーは、中央の数値が挿入される前のツリーの形状に関係なく、2つのノード上の2つの可能なツリーのいずれかになる可能性が等しくなります。
暗黙の treap [ 8 ]は、通常の treap の単純な変形であり、次の操作をサポートする動的配列と見なすことができます。:
暗黙的なtreapの背後にある考え方は、配列インデックスをキーとして使用するが、それを明示的に保存しないことである。そうしないと、更新(挿入/削除)によってキーが変更されることになる。ツリーのノード。
ノード T のキー値(暗黙のキー)は、そのノードより小さいノードの数に 1 を加えた数です。なお、このようなノードは、T が P の右部分木に含まれる場合、T の左部分木だけでなく、T の祖先 P の左部分木にも存在し得ます。
したがって、ツリーを下っていく際にすべてのノードの合計を累積することで、操作を実行する際に現在のノードの暗黙のキーを素早く計算できます。この合計は左サブツリーを訪問しても変化しませんが、増加します。適切なサブツリーを訪れるとき。
次の定義を考えてみましょう。
import std ;class ImplicitTreap { private : int key ; int prior ; ImplicitTreap * left ; ImplicitTreap * right ; public : explicit ImplicitTreap ( int key = 0 , int prior = std :: rand ()) : key { key }, prior { std :: rand ()}, left { nullptr }, right { nullptr } {}// ゲッターint count () const ; void updateCount (); void join ( ImplicitTreap * left , ImplicitTreap * right ); void split ( ImplicitTreap *& left , ImplicitTreap *& right ; int key , int add = 0 ); };暗黙のtreapの結合アルゴリズムは次のとおりです。[ 8 ]
void ImplicitTreap::join ( ImplicitTreap * left , ImplicitTreap * right ) { if ( ! left || ! right ) { this = left ? left : right ; } else if ( left -> getPrior () > right -> getPrior ()) { left -> getRight (). join ( left -> getRight , right ); this = left ; } else { right -> getLeft (). join ( left , right -> getLeft ()); this = right ; } updateCount (); }暗黙のtreapの分割アルゴリズムは次のとおりです。[ 8 ]
void ImplicitTreap::split ( ImplicitTreap *& left , ImplicitTreap *& right , int key , int add = 0 ) { int currentKey = add + this -> left . count (); //暗黙のキーif ( key <= currentKey ) { this -> left . split ( left , this -> left , key , add ); right = this ; } else { this -> right . split ( this -> right , right , key , add + 1 + this -> left . count ()); left = this ; } updateCount (); }位置posに要素を挿入するには、split関数を呼び出して配列を[0...pos-1]と[pos..sz]の 2 つのサブセクションに分割し、2 つのツリーを取得します。そして.次にマージしますjoin関数を呼び出すことで新しいノードと結合します。最後に、join 関数を呼び出してマージします。そして。
削除対象の要素を見つけ、その子要素であるLとRに対して結合操作を実行します。次に、削除対象の要素を、結合操作によって得られたツリーに置き換えます。
この計算を行うには、以下の手順で進めます。
この操作を実行するには、以下の手順で進めます。
特定のノードのサブツリーを各ノードごとに反転させる必要があることを示すために、追加のブール型フィールドRを作成し、その値をtrueに設定します。この変更を反映させるために、ノードの子ノードを入れ替え、すべての子ノードのRをtrueに設定します。
{{cite journal}}:ジャーナルを引用するには|journal=(ヘルプ)