BIT:バイナリインデックスツリーの背後にある直感とは何ですか?


99

バイナリインデックス付きツリーは、他のデータ構造と比較して、文献が非常に少ないか比較的少ないです。それが教えられる唯一の場所はtopcoderチュートリアルです。チュートリアルはすべての説明で完了していますが、そのようなツリーの背後にある直感を理解できませんか?どうやって発明されたのですか?その正確さの実際の証拠は何ですか?


4
ウィキペディアの記事では、これらはフェンウィックツリーと呼ばれています
デビッドハークネス

2
@ DavidHarkness- Peter Fenwickはデータ構造を発明したため、Fenwickツリーと呼ばれることもあります。彼の元の論文(citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.14.8917にあります)で、彼はそれらをバイナリインデックスツリーと呼びました。2つの用語は、しばしば同じ意味で使用されます。
templatetypedef

1
次の答えは、バイナリインデックスツリーcs.stackexchange.com/questions/42811/…の非常に優れた「視覚的」な直感を伝えています。
ラビコデイ

1
トップコーダーの記事を初めて読んだとき、あなたがどのように感じるか知っています。まるで魔法のように思えました。
Rockstar5645

回答:


168

直感的に、バイナリインデックスツリーは、それ自体が標準配列表現の最適化であるバイナリツリーの圧縮表現と考えることができます。この答えは、1つの可能な派生になります。

たとえば、合計7つの異なる要素の累積度数を保存するとします。最初に、番号が分配される7つのバケットを書き出すことで開始できます。

[   ] [   ] [   ] [   ] [   ] [   ] [   ]
  1     2     3     4     5     6     7

ここで、累積頻度が次のようになっていると仮定します。

[ 5 ] [ 6 ] [14 ] [25 ] [77 ] [105] [105]
  1     2     3     4     5     6     7

このバージョンの配列を使用すると、その場所に格納されている数値の値を増やし、その後に来るすべての要素の頻度を増やすことで、要素の累積頻度を増やすことができます。たとえば、次のように、3の累積頻度を7増やすために、位置3以降の配列の各要素に7を追加できます。

[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

これの問題は、これを行うのにO(n)時間かかることで、nが大きい場合はかなり遅いです。

この操作の改善について考える1つの方法は、バケットに保存するものを変更することです。指定したポイントまでの累積頻度を保存するのではなく、現在の頻度が前のバケットと比較して増加した量を保存することを考えることができます。たとえば、この場合、上記のバケットを次のように書き換えます。

Before:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

After:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

これで、適切な量をそのバケットに追加するだけで、時間O(1)にバケット内の頻度を増やすことができます。ただし、すべての小さいバケットの値を合計してバケットの合計を再計算する必要があるため、ルックアップの合計コストはO(n)になります。

ここからバイナリインデックス付きツリーを取得するために必要な最初の主要な洞察は次のとおりです。特定の要素に先行する配列要素の合計を継続的に再計算するのではなく、特定の要素の前にすべての要素の合計を事前に計算した場合シーケンス内のポイント?それができれば、これらの事前計算された合計の正しい組み合わせを合計するだけで、ある時点での累積合計を計算できます。

これを行う1つの方法は、表現をバケットの配列からノードのバイナリツリーに変更することです。各ノードには、そのノードの左側にあるすべてのノードの累積合計を表す値が注釈として付けられます。たとえば、これらのノードから次のバイナリツリーを構築するとします。

             4
          /     \
         2       6
        / \     / \
       1   3   5   7

これで、そのノードとその左のサブツリーを含むすべての値の累積合計を保存することにより、各ノードを拡張できます。たとえば、値を指定すると、次を保存します。

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]

このツリー構造を考えると、あるポイントまでの累積合計を簡単に決定できます。考え方は次のとおりです。最初は0のカウンターを維持し、問題のノードが見つかるまで通常のバイナリ検索を実行します。その際、次のことも行います。右に移動するたびに、現在の値をカウンターに追加します。

たとえば、3の合計を検索するとします。これを行うには、次のようにします。

  • ルート(4)から開始します。カウンターは0です。
  • ノード(2)に左に行きます。カウンターは0です。
  • ノード(3)を右に移動します。カウンターは0 + 6 = 6です。
  • ノード(3)を見つけます。カウンターは6 + 15 = 21です。

このプロセスを逆に実行することも想像できます。特定のノードで開始し、カウンターをそのノードの値に初期化してから、ツリーをルートまでたどります。正しい子リンクを上にたどるたびに、到達したノードの値を追加します。たとえば、3の頻度を見つけるには、次のようにします。

  • ノード(3)から開始します。カウンターは15です。
  • ノード(2)に移動します。カウンターは15 + 6 = 21です。
  • ノード(4)に移動します。カウンターは21です。

ノードの頻度(および暗黙的に、その後に来るすべてのノードの頻度)を増やすには、左のサブツリーにそのノードを含むツリーのノードのセットを更新する必要があります。これを行うには、次の手順を実行します。そのノードの頻度を増やしてから、ツリーのルートまで歩き始めます。左の子として表示されるリンクをたどるたびに、現在の値を追加して、発生したノードの頻度を増やします。

たとえば、ノード1の頻度を5ずつ増やすには、次のようにします。

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]

ノード1から開始して、頻度を5増やして取得します

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [+10] [+15] [+52] [ +0]

次に、その親に移動します。

                 4
               [+32]
              /     \
         > 2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

左の子リンクを上にたどったので、このノードの頻度も増やします。

                 4
               [+32]
              /     \
         > 2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

次に、その親に移動します。

               > 4
               [+32]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

これは左の子リンクであったため、このノードもインクリメントします。

                 4
               [+37]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

これで完了です!

最後のステップは、これからバイナリインデックス付きツリーに変換することです。ここで、バイナリ数を使っていくつかの楽しいことを行うことができます。このツリーの各バケットインデックスをバイナリで書き換えましょう。

                100
               [+37]
              /     \
          010         110
         [+11]       [+80]
         /   \       /   \
       001   011   101   111
      [+10] [+15] [+52] [ +0]

ここで、非常にクールな観測を行うことができます。これらの2進数のいずれかを取得し、数値に設定された最後の1を見つけて、そのビットとその後に続くすべてのビットをドロップします。これで、次のことができます。

              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]

0を「左」を意味し、1を「右」を意味する場合、各数字の残りのビットは、ルートから開始してその数字まで歩く方法を正確に説明します。たとえば、ノード5のバイナリパターンは101です。最後の1は最後のビットです。したがって、10を取得するためにそれをドロップします。実際、ルートで開始し、右(1)、左(0)で終了ノード5で!

これが重要な理由は、ルックアップおよび更新操作が、ノードからルートに戻るアクセスパスと、左または右の子リンクをたどっているかどうかに依存するためです。たとえば、ルックアップ中、私たちはたどる正しいリンクだけを気にします。更新中は、私たちがたどる左のリンクだけを気にします。このバイナリインデックスツリーは、インデックス内のビットを使用するだけで、このすべてを非常に効率的に実行します。

重要なトリックは、この完全なバイナリツリーの次のプロパティです。

ノードnが与えられた場合、nのバイナリ表現を取り、最後の1を削除することにより、アクセスパス上の次のノードに戻ります。

たとえば、ノード7のアクセスパスを見てみましょう。これは111です。ルートへのアクセスパス上のノードは、右ポインターを上方向にたどることになります。

  • ノード7:111
  • ノード6:110
  • ノード4:100

これらはすべて正しいリンクです。ノード3のアクセスパス(011)を使用して、右に進むノードを見ると、

  • ノード3:011
  • ノード2:010
  • (ノード4:100、左リンクをたどる)

これは、次のようにノードまでの累積合計を非常に効率的に計算できることを意味します。

  • ノードnをバイナリで書き込みます。
  • カウンターを0に設定します。
  • n≠0の間に以下を繰り返します。
    • ノードnの値を追加します。
    • nから右端の1ビットをクリアします。

同様に、更新手順をどのように行うかについて考えてみましょう。これを行うには、アクセスパスをたどってルートに戻り、左リンクをたどったすべてのノードを更新します。基本的に上記のアルゴリズムを実行することでこれを行うことができますが、すべての1を0に、0を1に切り替えます。

バイナリインデックスツリーの最後のステップは、このビットごとのトリックのために、ツリーを明示的に保存する必要さえないことに注意することです。すべてのノードを長さnの配列に格納してから、ビット単位の調整手法を使用してツリーを暗黙的にナビゲートできます。実際、それはビット単位のインデックス付きツリーの機能です。ノードを配列に格納し、これらのビット単位のトリックを使用して、このツリーを上方向に効率的にシミュレートします。

お役に立てれば!



2番目の段落で私を失いました。7つの異なる要素の累積頻度はどういう意味ですか?
ジェイソンゴーマート

20
これは、私がインターネットで見つけたすべての情報源の中で、これまでのところこのトピックで読んだ最高の説明です。よくやった !
アンモルシンジャギ

2
フェンウィックはどのようにこれをスマートにしたのですか?
Rockstar5645

1
これは非常に素晴らしい説明ですが、フェンウィック自身の論文と同様に他のすべての説明と同じ問題に苦しんでおり、証拠を提供しません!
-DarthPaghius

3

フェンウィックのオリジナルの論文はもっと鮮明だと思います。@templatetypedefによる上記の答えには、完全なバイナリツリーのインデックス付けに関する「非常にクールな観察」が必要です。

フェンウィックは、質問ツリー内のすべてのノードの責任範囲は、最後に設定されたビットに従うと単純に述べました。

フェンウィックツリーノードの役割

たとえば、6==の最後のセットビットは00110「2ビット」であるため、2ノードの範囲を担当します。ため12== 01100それは4つのノードの範囲を担当するので、それは、「4ビット」です。

したがって、F(12)== F(01100)を照会するとき、ビットを1つずつ除去して取得しF(9:12) + F(1:8)ます。これはほとんど厳密な証拠ではありませんが、完全なバイナリツリーではなく、数値軸に単純に配置すると、各ノードの責任は何で、クエリコストがビットを設定します。

それでも不明な場合は、このペーパーをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.