1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ダブリングを用いて木のLCAを求める(Python)

Last updated at Posted at 2020-10-24

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行引けばよいです。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?