ダブリングを用いて木のLCAを求める(Python)
LCAを求める際にはオイラーツアーかダブリングを用いる。と述べている記事をよく見かけます。ダブリングの手法について、最初、なぜあのようなコードになるか
理解ができなかったので記録としてメモをします。
この記事では次のような問題を解きます。
- 無向な根のある木において、LCAを求めたい。これはたくさんのクエリが与えられる。
- nodeは0-indexedでrootnodeは0
ダブリングについて
過去に書いたSlideShareのAtCoder167Dをダブリングで解くなどを参考にしてください。(他に分かりやすい記事はたくさんあります)
簡単に言えば、(代表されるのは)規則的な遷移のあるグラフにおいて、軽い計算量で事前計算することで、あるノード$x$の$2^k$回目の遷移を$table[k][x]$を参照することで得ることができます。これを用いると任意の回数$a$に対してその経っているビットごとの遷移先を辿ることで高速に($log_{2}x$で)求めることができます。
SlideShareの図の引用になりますが、以下のように計算をします。
ダブリングを用いた根付き木におけるa個先の祖先の求め方
satanicさんの資料がとても分かりやすいです。全ノードにおける1つ先の親を、例えば$ノードx$について、$table[0][x]$とすることで、ダブリングの事前テーブルを計算します。この時、$node0の親は-1$などにしておきます。
※node 0の親は-1など存在しない値にせねばならず、0(自分自身)にしてはダメです。後のLCA判定では親が同じか?という基準で判定を行うため、node 0 の親を0としてしまうと、1つ下のノード$b$と同じ階層に扱われてしまいます。
以下、ノード$x$における$a$個前の祖先を$ancestor(x, a)$とします。
補題: 木におけるノード分辿った後の親ノード
この木のノード数が$numv$の場合、$numv-1$回辿った後の親は必ず$-1$になります。なぜなら、例えば、5個のノードで一番深い場合というのは$0-1-2-3-4$と接続されている場合なので、4回辿ると必ず根に到達します。
LCAの考え方
さて、ここでLCAの特徴を考えます。ここでは、ノード$11と13$のLCA(図では4)を求めたいとします。
まず、ノードを$u=11, v=13$とします。まず最初に、$uとv$の深さの差を求めます。これは、木の構造を作成する際にDFSなりBFSでたどる際に、distといった変数に入れておけば事前計算可能です。さて、深い方のノードが$u$とするなら深さの差は$dist(u) - dist(v) =2$です。
uを移動して$u = ancestor(u, 2)$のように、深さの差分の分だけ辿ります。これで、$u,v = 9,11$となり、お互いの深さ(dist)が同じになりました。求めるノードの深さを同じにするは、LCAを求める際のポイントです。
深さを合わせることにより以下のようにLCAのノードを定義できます。$i=0,1,2...としていった際に、最初にancestor(u,i) == ancestor(v,i)となる、ancestor(uあるいはv, i)のノードがLCAである$。
また、次のようにも言い換えられます。
$i=0,1,2...としていった際に、最後にancestor(u,i) != ancestor(v,i)であるiに対して、ancestor(uあるいはv, i+1)のノードがLCAである$。
つまり、iを0以上の適当な数としたとき、以下の事が言えます。
ancestor(u,i) == ancestor(v,i)であるなら、これがLCAノードであるか(図の水色)、あるいは、辿りすぎており、より下にLCAがある(図の紫)
ancestor(u,i) != ancestor(v,i)であるなら、LCAノードはこれより上にある(図のピンク)
LCAを求める考察
木が十分に小さい場合、ある同じ深さのノード$u, v$からスタートして、$u!=v$である限り、$u=ancestor(u,1), v=ancestor(v,1)$してやればよいです。つまり、
while u != v:
u, v =ancestor(u,1), ancestor(v,1)
です。ただし、これでは$O(N)$かかってしまうため、大量のクエリには応えられません。
まず、シンプルな二分探索を考えます。例えば、上記の図のように深さが7であるなら、$l,r = 0,7$のように設定し、lにおいて$u,v$のancestorは異なり、rのとき$u,v$のancestorが同じになるようになるような$l,r$を探してやればよいです。この探索は$O(logN)$で動作しますが、各探索中にダブリングの計算には一定の計算量がかかります。
二分探索の場合$mid=(l+r)//2$としますがここで、深さ$15,7,3,1$を探索すると、ダブリングのテーブルを引く回数は(立っているビットだけの探索で)$4+3+2+1$回、tableを引く計算が起こります。
計算量を落とす工夫
そこで、次のようにancestorの計算をしていきます。まず、最初に、$u,v$の深さを$depth$としたとき、$k$をdepth以上の最も近い2のべき乗2**k
の$k$とします。例えば、深さ7であればその数以上の近い2のべき乗は8であるためk=3, 深さ8の場合もk=3, 深さ9の場合はその数以上の2のべき乗は16なのでk=4とします。
この時、 $ancestor(u,2^k) == ancestor(v,2^k)$ です。(深さdepth以上の値なので、ルートノードか-1になります)。このkを用いて、以下のような処理を行います。
for i in range(k, -1, -1): # iをk, k-1, ... , 1, 0で回す
nextu = ancestor(u, 2**i) # = table[i][u]
nextv = ancestor(v, 2**i) # = table[i][v]
if nextu != nextv:
u = nextu
v = nextv
return ancestor(u, 1)
ancestorは、$O(1)$の処理です。これは$table[i][node]$そのものであるからです。
この処理の概要は次の通りです。
- 処理1: iに対して$i=kから0まで$、$2^{i}$の祖先を$u,v$に対して辿る。これを$nextu, nextv$とする。
- 処理2-1: $nextu==nextv$の場合、LCAはもっと手前にあるはずなのだから(あるいはそれがLCAなのだから)、$u,v$はそのままにして、$i -= 1$して処理1に戻る
- 処理2-2: $nextu!=nextv$の場合、LCAはもっと上にあることは明確なのだから、$u,v$を$nextu, nextv$にして、$i-=1$して処理1に戻る
図の例で示すと、
- u,vがそれぞれ、$8,4,2,1$個上に登ってよいかを確かめる。$nextu, nextv$が異なる場合は登ってよく、$nextu, nextv$が同じ場合は登ってはならない。
この処理も二分探索ですが、各確認に必要な計算量は$O(1)$となります。
まとめ
- 木において、ダブリングを用いて$a$個先の祖先を$O(log a)$で求めることができます
- LCAは求めたいu,vの深さを合わせたうえで、ancestorが同じ間辿り続け、ancestorが違うようになるノードがLCAと分かりました
- これは二分探索で行うことが可能ですが、深さが2の階乗でないノードに対して行う場合、さらに計算量を減らすことができます。なぜなら、ある深さ$d$のancestorを求めるには、$d$の立っているbit分のテーブルを引くことが必要になるからです。
- 深さ$depth$ではなくて、深さ$depth$以上の最も近い深さから探索を開始します。これにより、事前計算したダブリングテーブルの行を1行引けばよいです。
Author And Source
この問題について(ダブリングを用いて木のLCAを求める(Python)), 我々は、より多くの情報をここで見つけました https://qiita.com/recuraki/items/2e6e4da3967963c3283a著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .