親インデックスは O(log * n) 時間と O(1) 空間で見つかります。
ここで、 log * nは反復対数を意味します。結果が 1 以下になる前に、対数関数を繰り返し適用する必要がある回数です。
実際には、O(1) 時間で、大きなルックアップ テーブル (ツリー内の各ノードの親インデックスを格納する) 用に O(n) スペースを確保できれば、さらに高速に実行できる可能性があります。
以下に、余分なスペースを必要としないいくつかのアルゴリズムをスケッチし、O(log n) 最悪の場合の時間、O(log log n) の予想時間、O(log log n) の最悪の場合の時間、および O(log * n) 最悪の場合の時間。これらは、完全な二分木のための順序付けされたインデックスの次のプロパティに基づいています。
- ツリーの一番左のパスにあるすべてのインデックスは 2 i -1に等しくなります。
- 一番左のパス上のノードのすべての右の子のインデックスは、2 i -2に等しくなります。
- 一番左のパスとその右のサブツリー上のすべてのノードは、同じ位置に最上位のゼロ以外のビットを持つインデックスでラベル付けされます:
i
.
- 一番左のパス上の任意のノードの左サブツリーには、2 i -1 ノードが含まれます。(つまり、2 i -1 を差し引いた後、親に対して同様の方法で配置され、同じ深さを持つが、プロパティ #1 と #2 を満たす「特別な」ノードに近いノードが得られます)。
プロパティ#1 と #2 は、ツリーのいくつかのノードの親ノードを取得するための単純なアルゴリズムを提供します。2 i -2形式のインデックスの場合は、を追加するだけです。他のノードについては、プロパティ #4 を繰り返し使用して、プロパティ #1 または #2 を満たすノードに到達し (いくつかの左側のサブツリーのサイズを減算することによって)、次に左端のパスにある親ノードを見つけて、以前のすべてを追加し直すことができます。減算値。また、プロパティ #3 を使用すると、差し引く必要があるサブツリーのサイズをすばやく見つけることができます。したがって、次のアルゴリズムがあります。2
1
1
- ながら
((x+1) & x) != 0
、((x+2) & (x+1)) != 0
手順 2 を繰り返します。
- ゼロ以外の最上位ビットをクリアし、 を追加し
1
ます。差額を積み上げます。
- の場合
((x+1) & x) == 0
、 を掛けて2
加算し1
ます。そうでない場合((x+2) & (x+1)) == 0
は、追加し1
ます。
- ステップ 2 で蓄積されたすべての差額を元に戻します。
たとえば、12
(2 進形式の0b1100
) はステップ 2 で に変換され0b0101
、次に0b0010
(または2
10 進数) に変換されます。累計差額は10
です。ステップ #3 が を追加1
し、ステップ #4 が追加さ 10
れるため、結果は になり13
ます。
その他の例: 10
(in binary form 0b1010
) は、ステップ #2 で0b0011
(または3
in decimal) に変換されます。ステップ #3 を 2 倍にして ( 6
)、追加します1
( 7
)。ステップ #4 は累積差 ( 7
) を加算するため、結果は になり14
ます。
時間の複雑さは O(log n) ですが、すべての基本操作が O(1) 時間で実行される場合のみです。
時間の複雑さを改善するために、ステップ 2 の反復を複数回並行して実行できます。n/2
インデックスの上位ビットを取得し、それらの人口数を計算できます。結果を残りの下位ビットに追加した後、合計がこれらの上位ビットにオーバーフローしない場合、O(log log n) の複雑さでこのアプローチを再帰的に適用できます。オーバーフローが発生した場合は、元のアルゴリズムに (ビット単位で) ロールバックできます。セットされたすべての下位ビットもオーバーフローとして扱われるべきであることに注意してください。したがって、結果の複雑さは O(log log n) expected timeです。
ビットごとにロールバックする代わりに、二分探索を使用してオーバーフローを処理できます。n/2
オーバーフローが発生しないように、または ( index に関して0b00101111111111
は) 選択されたゼロ以外の上位ビットの数が正確に になるように、いくつの上位ビット ( 未満) を選択するかを決定でき1
ます。数のビット数が O(log log n) より大きい場合、2 番目のオーバーフローは発生しないため、このバイナリ検索手順を複数回適用する必要がないことに注意してください。したがって、結果の複雑さは O(log log n)最悪の場合の時間です。すべての基本操作は O(1) 時間で実行されると想定されます。いくつかの操作 (人口カウント、先行ゼロカウント) が O(log log n) 時間で実装される場合、時間の複雑さは O(log 2 log n) に増加します。
インデックスのビットを 2 つの等しいセットに分割する代わりに、別の戦略を使用できます。
- インデックスの人口数を計算し、それをインデックス値に追加します。
0
からに変化した最上位ビットにより、上位1
/下位ビットの区切り点が決まります。
- 上位ビットで人口カウントを計算し、その結果を下位ビットに追加します。
- 「分離」ビットが非ゼロで
((x+1) & x) != 0
あり((x+2) & (x+1)) != 0
、かつ である場合は、ステップ #1 から続行します。
- すべての下位ビットが設定され、最下位の上位ビットが設定されている場合、このコーナー ケースを右の子として処理します。
((x+1) & x) != 0
との場合((x+2) & (x+1)) != 0
は、これも右の子として処理します。
- の場合
((x+1) & x) == 0
、 を掛けて2
加算し1
ます。そうでない場合((x+2) & (x+1)) == 0
は、追加し1
ます。
ステップ 3 の条件が満たされている場合、これはステップ 2 で加算した結果、「分離」ビットにキャリーが発生したことを意味します。他の下位ビットは、元の個体数を超えることのできない数値を表します。この数値に設定されるビット数は、元の値の人口カウントの対数より大きくすることはできません。これは、各反復後のセット ビット数が、前の反復でのセット ビット数の最大対数であることを意味します。したがって、最悪の場合の時間計算量は O(log * n) です。これは O(1) に非常に近いです。たとえば、32 ビットの数値の場合、必要な反復回数は約 2 回以下です。
このアルゴリズムのすべてのステップは明らかなはずですが、おそらくステップ 5 を除いて、その正確性が証明されます。このステップは、人口カウントを追加すると「分離」ビットにキャリーが発生する場合にのみ実行されますが、上位ビットのみの人口カウントを追加すると、このキャリーは発生しません。「分離」ビットは、値 2 iに対応します。全ビットの個体数と上位ビットのみの個体数の差は最大でi
です。したがって、ステップ 5 は少なくとも 2 i -i の値を扱います。この値にビットごとのアルゴリズムを適用してみましょう。2 ii-1
-i は、ビットが設定されるのに十分な大きさです。このビットをクリアし、ビット1
の値に加算します0..i-2
。2を引いたばかりなので、この値は少なくとも 2 i-1 -(i-1) です。i-1および を追加1
。または、インデックスを 1 つ右に移動すると、少なくとも 2 i -i になります。この手順を繰り返すと、常にゼロ以外のビットが位置 に見つかりますi-1
。0..i-1
各ステップでは、ビット単位の値と の最も近い累乗の差が徐々に減少します2
。この差が になると2
、現在のノードが明らかに正しい子であるため、このビットごとのアルゴリズムを停止できます。このビットごとのアルゴリズムは常に同じ結果になるため、それをスキップして現在のノードを常に正しい子として処理することができます。
このアルゴリズムの C++ 実装を次に示します。詳細とその他のアルゴリズムについては、ideoneを参照してください。
uint32_t getMSBmask(const uint32_t x)
{ return 1 << getMSBindex(x); }
uint32_t notSimpleCase(const uint32_t x)
{ return ((x+1) & x) && ((x+2) & (x+1)); }
uint32_t parent(const uint32_t node)
{
uint32_t x = node;
uint32_t bit = x;
while ((x & bit) && notSimpleCase(x))
{
const uint32_t y = x + popcnt(x);
bit = getMSBmask(y & ~x);
const uint32_t mask = (bit << 1) - 1;
const uint32_t z = (x & mask) + popcnt(x & ~mask);
if (z == mask && (x & (bit << 1)))
return node + 1;
x = z;
}
if (notSimpleCase(x))
return node + 1;
else
return node + 1 + (((x+1) & x)? 0: x);
}
葉ノードの親のみを見つける必要がある場合、このアルゴリズムとコードは単純化される可能性があります。(イデオネ)。
uint32_t parent(const uint32_t node)
{
uint32_t x = node;
while (x > 2)
{
const uint32_t y = x + popcnt(x);
const uint32_t bit = getMSBmask(y & ~x);
const uint32_t mask = (bit << 1) - 1;
x = (x & mask) + popcnt(x & ~mask);
}
return node + 1 + (x == 1);
}