バイナリインデックス付きツリーは、他のデータ構造と比較して、文献が非常に少ないか比較的少ないです。それが教えられる唯一の場所はtopcoderチュートリアルです。チュートリアルはすべての説明で完了していますが、そのようなツリーの背後にある直感を理解できませんか?どうやって発明されたのですか?その正確さの実際の証拠は何ですか?
バイナリインデックス付きツリーは、他のデータ構造と比較して、文献が非常に少ないか比較的少ないです。それが教えられる唯一の場所はtopcoderチュートリアルです。チュートリアルはすべての説明で完了していますが、そのようなツリーの背後にある直感を理解できませんか?どうやって発明されたのですか?その正確さの実際の証拠は何ですか?
回答:
直感的に、バイナリインデックスツリーは、それ自体が標準配列表現の最適化であるバイナリツリーの圧縮表現と考えることができます。この答えは、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の合計を検索するとします。これを行うには、次のようにします。
このプロセスを逆に実行することも想像できます。特定のノードで開始し、カウンターをそのノードの値に初期化してから、ツリーをルートまでたどります。正しい子リンクを上にたどるたびに、到達したノードの値を追加します。たとえば、3の頻度を見つけるには、次のようにします。
ノードの頻度(および暗黙的に、その後に来るすべてのノードの頻度)を増やすには、左のサブツリーにそのノードを含むツリーのノードのセットを更新する必要があります。これを行うには、次の手順を実行します。そのノードの頻度を増やしてから、ツリーのルートまで歩き始めます。左の子として表示されるリンクをたどるたびに、現在の値を追加して、発生したノードの頻度を増やします。
たとえば、ノード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です。ルートへのアクセスパス上のノードは、右ポインターを上方向にたどることになります。
これらはすべて正しいリンクです。ノード3のアクセスパス(011)を使用して、右に進むノードを見ると、
これは、次のようにノードまでの累積合計を非常に効率的に計算できることを意味します。
同様に、更新手順をどのように行うかについて考えてみましょう。これを行うには、アクセスパスをたどってルートに戻り、左リンクをたどったすべてのノードを更新します。基本的に上記のアルゴリズムを実行することでこれを行うことができますが、すべての1を0に、0を1に切り替えます。
バイナリインデックスツリーの最後のステップは、このビットごとのトリックのために、ツリーを明示的に保存する必要さえないことに注意することです。すべてのノードを長さnの配列に格納してから、ビット単位の調整手法を使用してツリーを暗黙的にナビゲートできます。実際、それはビット単位のインデックス付きツリーの機能です。ノードを配列に格納し、これらのビット単位のトリックを使用して、このツリーを上方向に効率的にシミュレートします。
お役に立てれば!
フェンウィックのオリジナルの論文はもっと鮮明だと思います。@templatetypedefによる上記の答えには、完全なバイナリツリーのインデックス付けに関する「非常にクールな観察」が必要です。
フェンウィックは、質問ツリー内のすべてのノードの責任範囲は、最後に設定されたビットに従うと単純に述べました。
たとえば、6
==の最後のセットビットは00110
「2ビット」であるため、2ノードの範囲を担当します。ため12
== 01100
それは4つのノードの範囲を担当するので、それは、「4ビット」です。
したがって、F(12)
== F(01100)
を照会するとき、ビットを1つずつ除去して取得しF(9:12) + F(1:8)
ます。これはほとんど厳密な証拠ではありませんが、完全なバイナリツリーではなく、数値軸に単純に配置すると、各ノードの責任は何で、クエリコストがビットを設定します。
それでも不明な場合は、このペーパーをお勧めします。