| 分離集合/結合探索フォレスト | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| タイプ | 多元木 | ||||||||||||||||||||
| 発明された | 1964 | ||||||||||||||||||||
| 発明者 | バーナード・A・ギャラーとマイケル・J・フィッシャー | ||||||||||||||||||||
| |||||||||||||||||||||
コンピュータサイエンスにおいて、素集合データ構造(素集合データ構造、または和集合検索データ構造、またはマージ集合検索集合とも呼ばれる)は、素集合(重複しない集合)の集合を格納するデータ構造です。これは、集合を素集合に分割したものを格納することと同等です。このデータ構造は、新しい集合の追加、集合のマージ(それらの和集合に置き換える)、そして集合の代表要素の検索といった操作を提供します。最後の操作により、任意の2つの要素が同じ集合に属するか異なる集合に属するかを効率的に判定できます。
分離集合データ構造を実装する方法はいくつかありますが、実際には、分離集合フォレストと呼ばれる特定の実装と同一視されることが多いです。この特殊なタイプのフォレストは、和集合操作と検索操作をほぼ一定の償却時間で実行します。 n個のノードを持つ分離集合フォレストでのm回の加算、和集合、または検索操作のシーケンスに必要な合計時間はO ( m α( n ))です。ここで、α( n )は非常にゆっくりと増加する逆アッカーマン関数です。分離集合フォレストは操作ごとにこの時間を保証しませんが、各操作は構造を再調整します (ツリー圧縮経由)。その結果、分離集合フォレストは漸近的に最適であり、実質的に効率的です。
分離集合データ構造は、グラフの最小全域木を求めるクラスカルのアルゴリズムにおいて重要な役割を果たします。最小全域木の重要性は、分離集合データ構造が幅広いアルゴリズムをサポートしていることを意味します。さらに、これらのデータ構造は、記号計算やコンパイラ、特にレジスタ割り当て問題にも応用されています。
分離集合フォレストは、1964 年にBernard A. GallerとMichael J. Fischerによって初めて説明されました。 [ 2 ] 1973 年に、その時間計算量はの反復対数に制限され、HopcroftとUllmanによって制限されました。[ 3 ] 1975 年に、Robert Tarjan が、アルゴリズムの時間計算量の(逆アッカーマン関数) 上限を初めて証明しました。 [ 4 ]彼はまた、それが厳密であることを証明しました。1979 年に、彼はこれがGaller-Fischer 構造を含むポインタ アルゴリズムなどの特定のクラスのアルゴリズムの下限であることを示しました。 [ 5 ] 1989 年に、FredmanとSaks は、ビットの (償却) ワードは任意の分離集合データ構造ごとに操作ごとにアクセスする必要があることを示しました。 [ 6 ]
1991年にガリルとイタリアーノは分離集合のデータ構造の調査を発表しました。[ 7 ]
1994年、リチャード・J・アンダーソンとヘザー・ウォルは、ブロックを必要としないUnion-Findの並列化バージョンを説明した。[ 8 ]
2007年、シルヴァン・コンションとジャン=クリストフ・フィリアトルは、分離集合フォレストデータ構造の半永続版を開発し、証明支援ツールRocq(当時はCoq)を用いてその正しさを形式化しました。[ 9 ]「半永続」とは、構造の以前のバージョンは効率的に保持されるものの、以前のバージョンのデータ構造にアクセスすると、それ以降のバージョンは無効になることを意味します。彼らの最速の実装は、非永続アルゴリズムとほぼ同等の効率性を達成しています。彼らは計算量解析を行っていません。
限定されたクラスの問題に対してより優れた性能を発揮する、分離集合データ構造の変種も検討されている。ガボウとタージャンは、可能な和集合が特定の方法で制限されている場合、真に線形時間アルゴリズムが可能であることを示した。[ 10 ]特に、「和集合木」が事前に与えられていれば、線形時間を達成できる。これは、集合のすべての要素を含む木である。p[ v ]を木における親とすると、和集合演算は、あるvに対してunion ( v ,p[ v ])という形式をとる必要があるという仮定が成り立つ。
このセクションと次のセクションでは、分離集合データ構造の最も一般的な実装である、 親ポインタツリーのフォレストについて説明します。この表現はガラー・フィッシャー木として知られています。
分離セット フォレスト内の各ノードは、ポインターと、サイズまたはランク (両方ではない) などの補助情報で構成されます。ポインターは、親ポインター ツリーを作成するために使用されます。親ポインターツリーでは、ツリーのルートではない各ノードがその親を指します。ルート ノードを他のノードと区別するために、親ポインターには、ノードへの循環参照やセンチネル値などの無効な値が含まれます。各ツリーはフォレストに格納されているセットを表し、セットのメンバーはツリー内のノードです。ルート ノードはセットの代表を提供します。2 つのノードが同じセットに含まれるのは、そのノードを含むツリーのルートが等しい場合のみです。
フォレスト内のノードは、アプリケーションにとって都合の良い方法で保存できますが、一般的な手法は配列に保存することです。この場合、親は配列のインデックスで指定できます。配列の各エントリは、親ポインター用にΘ(log n )ビットのストレージを必要とします。エントリの残りの部分には、同等かそれ以下のストレージが必要となるため、フォレストを保存するために必要なビット数はΘ( n log n )です。実装で固定サイズのノードを使用する場合(これにより、保存できるフォレストの最大サイズが制限されます)、必要なストレージはnに比例します。
分離セット データ構造は、新しい要素を含む新しいセットの作成、特定の要素を含むセットの代表の検索、および 2 つのセットのマージという 3 つの操作をサポートします。
このMakeSet操作は、新しい要素のみを含む新しい集合に新しい要素を追加し、その新しい集合をデータ構造に追加します。データ構造を集合の分割と見なすと、このMakeSet操作は新しい要素を追加することで集合を拡大し、新しい要素のみを含む新しい部分集合に新しい要素を配置することで既存の分割を拡張します。
分離集合フォレストでは、MakeSetノードの親ポインタとノードのサイズまたはランクを初期化します。ルートが自身を指すノードで表される場合、要素の追加は次の擬似コードで記述できます。
関数MakeSet( x )は、 x がフォレスト内に存在しない場合、x .parent := x x .size := 1 // ノードがサイズを格納する場合、x .rank := 0 // ノードがランクを格納する場合、 end if end function
この操作は線形時間計算量を持ちます。特に、n個のノードを持つ分離集合フォレストの初期化にはO ( n )の 時間が必要です。
ノードに親が割り当てられていない場合、そのノードはフォレスト内に存在しないことを意味します。
実際には、x をMakeSet保持するためのメモリ割り当て操作が先行する必要があります。メモリ割り当てが、優れた動的配列実装と同様に、償却定数時間操作である限り、ランダムセットフォレストの漸近的なパフォーマンスは変化しません。
この操作は、指定されたクエリノードxFindから親ポインタの連鎖を辿り、ルート要素に到達します。このルート要素はxが属する集合を表し、 x自身である場合もあります。 到達したルート要素を返します。 Find
操作を実行することは、Findフォレストを改善する重要な機会となります。操作の時間Findは親ポインタの追跡に費やされるため、ツリーが平坦であればFind操作は高速化されます。 a がFind実行されると、各親ポインタを順にたどるよりも速くルートに到達する方法はありません。ただし、この検索中に参照される親ポインタは、ルートに近い位置を指すように更新できます。ルートへの途中で参照されるすべての要素は同じセットの一部であるため、フォレストに格納されているセットは変更されません。ただし、Findクエリノードとルートの間にあるノードだけでなく、その子孫に対しても、将来の操作が高速化されます。この更新は、分離セットフォレストの償却パフォーマンス保証の重要な部分です。
漸近的に最適な時間計算量を達成するアルゴリズムはいくつかあります。パス圧縮Findと呼ばれるアルゴリズム群は、クエリノードとルートノードの間にあるすべてのノードをルートノードに向けます。パス圧縮は、次のように単純な再帰を用いて実装できます。
関数Find( x )は、x .parent ≠ xの場合、x .parent := Find( x .parent ) を返し、そうでない場合はxを返します。
この実装は2つのパス、つまりツリーを1つ上へ、もう1つ下へ移動します。クエリノードからルートまでのパスを格納するのに十分なスクラッチメモリが必要です(上記の擬似コードでは、パスはコールスタックを使用して暗黙的に表現されています)。両方のパスを同じ方向に実行することで、メモリ使用量を一定量に減らすことができます。この定数メモリ実装では、クエリノードからルートまで2回移動します。1回はルートを見つけるため、もう1回はポインタを更新するためです。
関数Find( x )はroot := xであり、root .parent ≠ rootである。root := root .parent を実行すれば、x .parent ≠ root のときに、 parent := x .parent x .parent := root x := parentを実行します。ルート終了関数を返す
TarjanとVan LeeuwenはFind、最悪のケースにおける複雑さはそのままに、実用上はより効率的なワンパスアルゴリズムも開発しました。 [ 4 ] これらはパス分割とパス半減と呼ばれます。どちらも、クエリノードとルートノード間のパス上にあるノードの親ポインタを更新します。 パス分割は、そのパス上にあるすべての親ポインタを、そのノードの祖父母へのポインタに置き換えます。
関数Find( x )は、x .parent ≠ xの間、 do ( x , x .parent) := ( x .parent, x .parent.parent) を実行し、xを返して関数を終了します。
パスの半分化も同様に動作しますが、他のすべての親ポインターのみを置き換えます。
関数Find( x )は、x .parent ≠ xの間、 x .parent := x .parent.parent x := x .parent を実行し、 xを返しながら関数を終了します。
この操作は、 x を含む集合とyを含む集合をそれらの和集合に置き換えます。 まず、xとyを含む木の根を決定するためにを使用します。根が同じであれば、それ以上何もする必要はありません。そうでない場合は、2つの木をマージする必要があります。これは、xの根の親ポインタをyの根に設定するか、 yの根の親ポインタをxの根に設定することで行われます。 Union(x, y)UnionFind
どのノードを親ノードにするかの選択は、ツリーに対する将来の操作の複雑さに影響を与えます。不注意に選択すると、ツリーが過度に高くなる可能性があります。例えば、xUnionを含むツリーを常にyを含むツリーのサブツリーにするとします。要素で初期化したばかりのフォレストから、, , ...,を実行します。結果として得られるフォレストには、ルートがnである単一のツリーが含まれ、 1 からnへのパスはツリー内のすべてのノードを通過します。このフォレストの実行時間はO ( n )です。 Union(1, 2)Union(2, 3)Union(n - 1, n)Find(1)
効率的な実装では、木の高さはサイズによる結合またはランクによる結合によって制御されます。どちらの方法でも、ノードは親ノードへのポインタ以外の情報を保存する必要があります。この情報は、どのルートが新しい親ノードになるかを決定するために使用されます。どちらの戦略も、木が深くなりすぎないようにします。
サイズによる結合の場合、ノードはそのサイズ、つまり子孫の数(ノード自身を含む)を保持します。ルートxとyを持つツリーを結合する場合、子孫の数が多い方のノードが親になります。2つのノードの子孫の数が同じ場合は、どちらが親になることもできます。どちらの場合も、新しい親ノードのサイズは、新しい子孫の総数に設定されます。
function Union( x , y )は// ノードをルートに置き換えるx := Find( x ) y := Find( y ) x = y の場合return // x と y はすでに同じ集合内にあるend if// 必要であれば、変数を交換して、x が少なくとも y と同じ数の子孫を持つようにします。if x .size < y .size then ( x , y ) := ( y , x ) end if// x を新しいルートにするy .parent := x // x のサイズを更新するx .size := x .size + y .size 関数の終了
サイズを格納するために必要なビット数は、明らかにnを格納するために必要なビット数です。これにより、フォレストに必要なストレージ容量に定数倍が追加されます。
ランクによる結合では、ノードはその高さの上限であるランクを格納します。ノードが初期化されると、ランクは 0 に設定されます。ルートxとyを持つツリーをマージするには、まずそれらのランクを比較します。ランクが異なる場合、ランクの大きい方のツリーが親になり、xとyのランクは変わりません。ランクが同じ場合、どちらが親になることもできますが、新しい親のランクは 1 ずつ増加します。ノードのランクは明らかにその高さに関連していますが、高さを格納するよりもランクを格納する方が効率的です。ノードの高さはFind操作中に変わることがあるため、ランクを格納することで高さを正確に保つための余分な労力を省くことができます。疑似コードでは、ランクによる結合は次のようになります。
function Union( x , y )は// ノードをルートに置き換えるx := Find( x ) y := Find( y ) x = y の場合return // x と y はすでに同じ集合内にあるend if// 必要であれば、変数名を変更して、x のランクが y のランク以上になるようにします。if x .rank < y .rank then ( x , y ) := ( y , x ) end if// x を新しいルートにするy .parent := x // 必要であれば、x のランクをインクリメントするif x .rank = y .rank then x .rank := x .rank + 1 end if end function
すべてのノードはランク以下であることが示される。 [ 11 ]したがって、各ランクはO (log log n )ビット で格納でき、すべてのランクはO ( n log log n )ビットで格納できる。これにより、ランクはフォレストのサイズに対して漸近的に無視できるほど小さい割合となる。
上記の実装から明らかなように、ノードがツリーのルートでない限り、ノードのサイズとランクは重要ではありません。ノードが子ノードになると、そのサイズとランクは再びアクセスされることはありません。
Unionユーザーが形成された集合の代表値を決定するという操作 のバリエーションがあります。この機能を上記のアルゴリズムに追加しても、効率性を損なうことなく容易に実現できます。
Find親ポインタを更新せず、Union木の高さを制御しない分離集合フォレスト実装では、高さがO ( n )のツリーを持つことができます。このような状況では、Findと のUnion操作にはO ( n ) の処理時間がかかります。
実装がパス圧縮のみを使用する場合、n回のMakeSet操作のシーケンスの後に最大n − 1回のUnion操作とf回のFind操作が続く場合、最悪の場合の実行時間は0になります。[ 11 ]
ランクによる結合を使用し、実行中に親ポインタを更新しない場合、任意のタイプのm個の操作(最大n個の操作)Findの実行時間は になります。[ 11 ]MakeSet
パスの圧縮、分割、半分化と、サイズまたはランクによる結合を組み合わせると、最大n個の演算を含む任意のタイプのm個の演算の実行時間が に短縮されます。[ 4 ] [ 5 ] これにより、各演算の償却実行時間は になります。これは漸近的に最適であり、すべての分離セットのデータ構造では、演算ごとに償却時間を使用する必要があります。[ 6 ] ここで、関数 は逆アッカーマン関数です。逆アッカーマン関数は非常にゆっくりと増加するため、この係数は、物理的宇宙に実際に記述できる任意のnに対して4以下になります。これにより、分離セット演算は実質的に償却定数時間になります。 MakeSet
分離集合フォレストの性能を正確に分析するのはやや複雑です。しかし、n個のオブジェクトを含む分離集合フォレストにおける任意のmFind個の操作の償却時間はO ( m log * n )であることが証明される、はるかに単純な分析があります。ここで、log *は反復対数を表します。[ 12 ] [ 13 ] [ 14 ] [ 15 ]Union
補題 1: find 関数がルートに沿ってパスをたどるにつれて、遭遇するノードのランクが増加します。
データセットに Find 操作と Union 操作を適用しても、この事実は時間の経過とともに変わりません。各ノードが自身のツリーのルートである場合、これは自明です。ノードのランクが変化する可能性があるのは、Union by Rank操作が適用された場合のみです。この場合、ランクの低いツリーがランクの高いツリーに接続され、その逆は発生しません。また、Find 操作中は、パスに沿って訪問されるすべてのノードがルートに接続されます。ルートのランクは子ノードよりも高いため、この操作によってもこの事実は変わりません。
補題2: ランクrのサブツリーのルートであるノードuには少なくともノードがある。
各ノードが自身の木の根である場合、これは自明です。ランクrのノードuには少なくとも2 r個のノードがあると仮定します。ランクrの2つの木をUnion by Rank演算で結合すると、ランクr + 1の木が生成され、その根には少なくともノードが含まれます。

補題3: ランクrのノードの最大数は、
補題2から、ランクrの部分木の根ノードuには少なくとも個のノードがあることが分かる。ランクrのノードの最大数は、ランクrの各ノードがちょうど個のノードを持つ木の根ノードであるときである。この場合、ランクrのノードの数は
実行中の任意の時点で、グラフの頂点をランクに応じて「バケット」にグループ化できます。バケットの範囲は、次のように帰納的に定義します。バケット0にはランク0の頂点が含まれます。バケット1にはランク1の頂点が含まれます。バケット2にはランク2と3の頂点が含まれます。一般に、B番目のバケットに区間 のランクを持つ頂点が含まれる場合、(B+1)番目のバケットには区間 のランクを持つ頂点が含まれます。
の場合、 とします。すると、バケットには区間 内のランクを持つ頂点が存在します。

バケットのサイズについては 2 つの点に留意できます。
Fは実行された「検索」操作のリストを表し 、
すると、 m個の検索にかかる総コストは
各検索操作はルートに到達するトラバーサルを1回だけ行うため、T 1 = O ( m )となります。
また、バケットの数に関する上記の境界から、T 2 = O ( m log * n )となります。
T 3では、 uからvへのエッジをトラバースしていると仮定します。ここで、uとv はバケット[ B、 2 B − 1]内でランクを持ち、vはルートではありません (このトラバースの時点ではルートではありません。そうでなければ、トラバースはT 1で考慮されます)。u を固定し、さまざまな検索操作でvの役割を果たすシーケンスを検討します。パス圧縮とルートへのエッジを考慮していないため、このシーケンスには異なるノードのみが含まれ、補題 1により、このシーケンス内のノードのランクは厳密に増加していることがわかります。両方のノードがバケット内にあるため、シーケンスの長さk (ノードu が同じバケット内の異なるルートに接続されている回数) は、バケットB内のランクの数以下、つまり、最大で
したがって、
したがって、
ランクによる結合または重みによる結合を持つFind木における 操作の最悪時間は(つまり であり、この境界は厳密である)である。1985 年に、N. Blum はパス圧縮を使用せず、 の間に木を圧縮する 操作の実装を示した。彼の実装は操作あたり 時間で実行されるため、[ 16 ] Galler と Fischer の構造と比較すると、操作あたりの最悪時間は短縮されるが、償却時間は短縮される。1999 年に、Alstrup らは、逆アッカーマン償却時間とともに最悪時間が最適となる構造を示した。 [ 17 ]
通常の分離集合フォレストの実装では、要素の削除に対してあまり反応しません。つまり、Find要素数の減少によって の時間が短縮されないということです。しかし、定数時間の削除を可能にし、 の時間制限が現在の要素数Findに依存する現代的な実装も存在します[ 18 ] [ 19 ]
特定の分離集合フォレスト構造を拡張してバックトラッキングを可能にすることが可能です。バックトラッキングの基本的な形式は、 Backtrack(1)最後の を元に戻す操作を許可することですUnion。より高度な形式では、最後の i 個の結合を元に戻す が許可されます。次のような計算量の結果が知られています。操作ごとに と をサポートし、 とをBacktrack(i)サポートするデータ構造があります。[ 20 ]この結果において、 が形成された集合の代表を選択する自由度が不可欠です。分離可能なポインタアルゴリズムのクラスでは、より短い償却時間を実現することはできません。[ 20 ]UnionFindBacktrackUnion

分離集合データ構造は、集合の分割をモデル化します。例えば、無向グラフの連結成分を追跡するために使用されます。このモデルは、2つの頂点が同じ成分に属しているかどうか、あるいはそれらの間に辺を追加すると閉路になるかどうかを判定するために使用できます。Union-Findアルゴリズムは、ユニフィケーションの高性能実装で使用されます。[ 21 ]
このデータ構造は、Boost GraphライブラリのIncremental Connected Components機能を実装するために使用されます。また、グラフの 最小全域木を求めるKruskalアルゴリズムを実装するための重要なコンポーネントでもあります。
Hoshen -Kopelman アルゴリズムでは、アルゴリズム内で Union-Find を使用します。
集合結合問題の任意
のCPROBE(log n )実装では、 n個のシングルトン集合から始めて、 m回のFindとn −1回のUnionを実行するのにΩ( m α( m , n ))時間が必要です。