ラベルなしツリーの効率的な圧縮


20

ラベルのない、ルート付きのバイナリツリーを検討してください。私たちはできる圧縮サブツリーへのポインタがあるたび:そのような木をTTT=T(通訳=構造的平等など)、我々は保存(WLOG)Tとのすべてのポインタ置き換えるTへのポインタでT。例については、uliの回答を参照してください。

上記の意味でツリーを入力として使用し、圧縮後に残る(最小限の)ノード数を計算するアルゴリズムを提供します。このアルゴリズムは、入力のノード数nで、時間On log n O(nlogn)(均一コストモデル)で実行する必要があります。n

これは試験問題であり、良い解決策を思い付くことができず、見たこともありません。


そして、「コスト」、「時間」、ここでの基本操作は何ですか?訪問したノードの数は?横断したエッジの数は?そして、入力のサイズはどのように指定されますか?
ウリ

このツリー圧縮は、ハッシュ圧縮のインスタンスです。それが一般的なカウント方法につながるかどうかはわかりません。
ジル「SO-悪であるのをやめる」

@uli が何であるかを明確にしました。しかし、「時間」は十分に具体的だと思います。非並行設定では、これは、最も頻繁に発生する基本操作のカウントに相当するランダウ用語でのカウント操作に相当します。n
ラファエル

@Raphaelもちろん、意図した基本操作が何であるかを推測することができ、おそらく他の全員と同じものを選択するでしょう。しかし、「時間制限」が与えられているときはいつでも、何がカウントされているかを述べることが重要です。スワップ、比較、追加、メモリアクセス、検査されたノード、トラバースされたエッジ、名前を付けます。物理学で測定単位を省略するようなものです。10ですかまたは 1010kg?そして、ほとんどの場合、メモリアクセスが最も頻繁に行われる操作だと思います。10ms
ウリ

@uliこれらは、「均一コストモデル」が伝えることになっている詳細の一種です。どの操作が基本的であるかを正確に定義するのは苦痛ですが、ケースの99.99%(この操作を含む)にはあいまいさはありません。複雑度クラスには基本的に単位がありません。1つのインスタンスを実行するのにかかる時間は測定しませんが、入力が大きくなるとこの時間は変化します。
ジル「SO-悪であるのをやめる」

回答:


10

はい、この圧縮は時間で実行できますが、簡単ではありません:)最初にいくつかの観測を行ってから、アルゴリズムを示します。ツリーは最初は圧縮されていないと仮定します。これは実際には必要ではありませんが、分析を容易にします。Onログn

まず、「構造的平等」を帰納的に特徴付けます。ましょうTが 2(サブ)の木も。場合TおよびTは' NULL木(全く頂点を有さない)の両方であり、それらは構造的に等価です。もしTTTT Tは両方ともNULLでない木です、そして、彼らは左の子供が構造的に同等であり、その右の子供が構造的に等価である場合に限っ構造的に等価です。「構造的等価」は、これらの定義の最小の不動点です。TT

たとえば、2つのリーフノードは構造的に同等です。これは、両方の子が構造的に同等である両方の子としてnullツリーを持っているためです。

「彼らの左の子供は構造的に同等であり、彼らの右の子供も」と言うのはむしろ迷惑なので、しばしば「彼らの子供は構造的に同等」と言い、同じことを意図します。また、「この頂点をルートとするサブツリー」を意味するとき、「この頂点」と言うことがあります。

上記の定義は、圧縮の実行方法のヒントをすぐに提供します。深さが最大でのすべてのサブツリーの構造的等価性がわかっている場合、深さd + 1のサブツリーの構造的等価性を簡単に計算できます。O n 2)の実行時間を避けるために、この計算をスマートな方法で行う必要があります。dd+1On2

アルゴリズムは、実行中にすべての頂点に識別子を割り当てます。識別子は、セットの数であり。識別子は一意であり、変更されることはありません。したがって、アルゴリズムの開始時に何らかの(グローバル)変数を1に設定し、識別子を頂点に割り当てるたびに、その変数の現在の値を頂点に割り当て、インクリメントしますその変数の値。{123n}

最初に、入力ツリーを、親へのポインターとともに、同じ深さの頂点を含む(最大)リストに変換します。これはOで簡単にできますn時間ます。On

最初にすべてのリーフ(深さ0の頂点を持つリストでこれらのリーフを見つけることができます)を単一の頂点に圧縮します。この頂点に識別子を割り当てます。2つの頂点の圧縮は、いずれかの頂点の親をリダイレクトして、代わりに他の頂点を指すようにします。

次の2つの観測を行います。1つ目は、すべての頂点に厳密に小さい深度の子があり、2つ目は、dよりも小さい深度のすべての頂点に対して圧縮を実行した場合です。d(およびそれらの識別子を与えている)、そして深さの2つの頂点構造的に等価であり、子の識別子が一致する場合は圧縮できます。この最後の観察結果は次の引数から得られます:2つの頂点は、それらの子が構造的に同等である場合、構造的に同等です。圧縮後、これは、ポインターが同じ子を指していることを意味します。d

深さが小さいものから大きいものまで同じ深さのノードを使用して、すべてのリストを反復処理します。すべてのレベルについて、整数ペアのリストを作成します。すべてのペアは、そのレベルのある頂点の子の識別子に対応します。そのレベルの2つの頂点は、対応する整数ペアが等しい場合にのみ構造的に同等です。辞書編集順序を使用して、これらをソートし、等しい整数ペアのセットを取得できます。これらのセットを上記のように単一の頂点に圧縮し、それらに識別子を与えます。

上記の観察結果は、このアプローチが機能し、圧縮されたツリーが得られることを証明しています。合計実行時間はと作成したリストのソートに必要な時間です。作成する整数ペアの総数はnなので、必要に応じて、合計実行時間はO n log n になります。プロシージャの最後に残ったノードの数を数えるのは簡単です(配布した識別子の数を見てください)。OnnOnログn


私はあなたの答えを詳しく読んでいませんが、あなたはノードを調べる奇妙な問題特有の方法で、ハッシュコンシングを多かれ少なかれ再発明したと思います。
ジル 'SO-悪であるのをやめる

@Alex「厳密に小さい子degree」の学位はおそらくdepth?また、CSツリーが下に向かって成長しているにもかかわらず、「ツリーの高さ」は「ツリーの深さ」よりも混乱が少ないと感じています。
ウリ

いい答えだ。ソートを回避する方法があるべきだと思います。@Gillesの回答に関する2番目のコメントもここで有効です。
ラファエル

@uli:うん、あなたは正しい、私はそれを訂正しました(なぜ私がこれら2つの単語を混同したのかわかりません)。高さと深さは微妙に異なる2つの概念であり、後者が必要でした:)私はそれらを交換することで誰もが混乱するのではなく、従来の「深さ」に固執すると思いました。
アレックス10ブリンク

4

構造的に等しいサブタームを複製しないように、不変のデータ構造を圧縮することは、ハッシュコンシングと呼ばれます。これは、関数型プログラミングにおけるメモリ管理の重要な手法です。ハッシュコンシングは、データ構造の体系的なメモ化の一種です。

ツリーをハッシュコンスし、ハッシュコンシング後にノードをカウントします。サイズデータ構造をハッシュ化することは、常にO nnO(nlg(n))操作。最後のノードの数を数えることは、ノードの数に比例します。

ツリーは次の構造を持つと考えます(ここではHaskell構文で記述されています)。

data Tree = Leaf
          | Node Tree Tree

コンストラクタごとに、可能な引数からコンストラクタをこれらの引数に適用した結果へのマッピングを維持する必要があります。葉は取るに足らないものです。ノードの場合、有限部分マップを維持します。T × T Nここで、Tはツリー識別子のセット、Nはノード識別子のセットです。T = N { } nodes:T×TNTNT=N{}唯一のリーフ識別子です。(具体的には、識別子はメモリブロックへのポインタです。)

nodes平衡二分探索木など、対数時間データ構造を使用できます。以下ではlookup nodesnodesデータ構造内のキーを検索する操作を呼び出します。insert nodes操作と、新しいキーの下に値を追加してそのキーを返すます。

次に、ツリーを走査して、ノードを追加します。Haskellのような擬似コードで記述していますnodesが、グローバルな可変変数として扱います。追加するだけですが、挿入は全体にスレッド化する必要があります。add関数は、にそのサブツリーを追加して、ツリーに再帰的nodes地図、および根の識別子を返します。

insert (p1,p2) =
add Leaf = $\ell$
add (Node t1 t2) =
    let p1 = add t1
    let p2 = add t2
    case lookup nodes (p1,p2) of
      Nothing -> insert nodes (p1,p2)
      Just p -> p

insert呼び出しの数は、nodesデータ構造の最終サイズでもあり、最大圧縮後のノードの数です。(必要に応じて、空のツリー用に追加します。)


「サイズデータ構造をハッシュ化するハッシュは、O n l g n 操作で常に実行できます」のリファレンスを提供できますか?目的のランタイムを実現するには、バランスの取れたツリーが必要になることに注意してください。nOnlgnnodes
ラファエル

同じツリーのハッシュを個別に計算すると常に同じ結果が得られるように、構造化された方法で部分構造を数値にハッシュすることだけを考えていました。可変データ構造が手元にあれば、ソリューションも問題ありません。しかし、それは少しきれいにできると思います。インタリーブinsertadd、私見を明示的になされるべきであると、実際の問題を解決する機能が与えられるべきです。
ラファエル

1
@Raphaelハッシュコンシングは、ポインター/識別子のタプル上の有限マップ構造に依存しているため、ルックアップおよび追加の対数時間(バランスの取れたバイナリ検索ツリーなど)で実装できます。私のソリューションには可変性は必要ありません。nodes便宜上、可変変数を作成しますが、全体を通してスレッド化できます。完全なコードを提供するつもりはありません。これはそうではありません。
ジル「SO-悪であるのをやめる」

1
@Raphael Hashing構造は、任意の番号を割り当てるのではなく、少し危険です。均一コストモデルでは、何でも大きな整数にエンコードし、それに対して一定時間の操作を行うことができますが、これは現実的ではありません。現実の世界では、暗号化ハッシュを使用して、無限セットから有限範囲の整数への事実上の1対1マッピングを行うことができますが、速度は遅くなります。ハッシュとして非暗号化チェックサムを使用する場合、衝突について考える必要があります。
ジル 'SO-悪であるのをやめる

3

ここでは、単に任意にラベルを付けるのではなく、ツリーの構造を数値に(注入的に)エンコードすることを目的とする別のアイデアを示します。そのために、任意の数の素因数分解が一意であることを使用します。

ここでは、がツリー内の空の位置を示し、NがNEが左サブツリー lと右サブツリー rを持つノードを示すものとします。N E E は葉です。今、みましょうNlrlrNEE

fE=0fNlr=2fl3fr

を使用して、ツリーのボトムアップに含まれるすべてのサブツリーのセットを計算できます。すべてのノードで、子から取得したエンコードのセットをマージし、新しい数を追加します(子のエンコードから一定の時間で計算できます)。f

この最後の仮定は、実際のマシンでのストレッチです。この場合、べき乗の代わりにCantorのペアリング関数に似たものを使用することを好むでしょう。

このアルゴリズムの実行時間は、ツリーの構造に依存します(バランスの取れたツリーでは、の線形時間で組合を可能にする任意のセットの実装で)。一般的なツリーの場合、単純な分析を行う対数時間結合が必要になります。ただし、洗練された分析が役立つ場合があります。通常の最悪の場合のツリーである線形リストは、ここで On log n 時間を許可するため、最悪の場合がどのようなものであるかはそれほど明確ではないことに注意してください。OnログnOnログn


1

写真はコメントに使用できないため:

ここに画像の説明を入力してください

左上:入力ツリー

右上:ノード5および7をルートとするサブツリーも同型です。

左下と右:圧縮されたツリーは一意に定義されていません。

この場合、ツリーのサイズはから減少していることに注意してくださいT | あまりにも6 +7+5|T|6+|T|


これは確かに、目的の操作の一例です。元の参照と追加された参照を区別しない場合、最終的な例は同一であることに注意してください。
ラファエル

-1

編集:TとT 'は同じ親の子だったので質問を読みました。圧縮の定義も再帰的であると考えました。つまり、以前に圧縮した2つのサブツリーを圧縮できます。それが実際の質問でない場合、私の答えはうまくいかないかもしれません。

OnログnTn=2Tn/2+cn

def Comp(T):
   if T == null:
     return 0
   leftCount = Comp(T.left)
   rightCount = Comp(T.right)
   if leftCount == rightCount:
     if hasSameStructure(T.left, T.right):
       T.right = T.left
       return leftCount + 1
     else
       return leftCount + rightCount + 1    

where hasSameStructure()は、すでに圧縮された2つのサブツリーを線形時間で比較して、まったく同じ構造を持っているかどうかを確認する関数です。それぞれをトラバースし、他のサブツリーが実行するたびに1つのサブツリーに左の子があるかどうかをチェックする線形時間再帰関数を作成することは難しくありません。

nnr

Tn=Tn1+Tn2+O1 もし nnr
2Tn/2+On さもないと

サブツリーが兄弟ではない場合はどうなりますか?ケア((T1、T1)、(T2、T1))T1は、2番目の3番目のポインターを使用して2回保存できます。
ウリ

TT

質問では、2つのサブストレスが同型であると特定されていると述べています。同じ親を持つことについては何も言われていません。前の例((T1、T1)、(T1、T2))のように、サブツリーT1がツリーに3回出現する場合、2番目の出現は3番目のオークレンスを指すことで圧縮できます。
ウリ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.