(2019/09/15 20:50頃追記) ただAuxiliary Treeと書くと(一般的すぎて)齟齬を生む可能性が高いので、タイトル及び一部文章を変更しました。申し訳ないです。今回はLCAをベースに構築するAuxiliary Treeについて取り上げてます。
最近、LCAを元に構築する Auxiliary Tree を使う問題に出会ったので書いた。
CodeChef July Challenge 2019 で以下の問題があった。
自分は Auxiliary Tree を使わず解いたが、想定解法が Binarization of a tree + Centroid Decomposition on Edge + Auxiliary Tree だったらしい。
以下を参考にしている。
- 虚树学习笔记 - Sengxian's Blog
- 競技プログラミングにおけるLCA問題まとめ [auxiliary tree] - はまやんはまやんはまやん
- ICL1705 - Editorial - editorial - CodeChef Discuss
目次
LCAをベースに構築するAuxiliary Treeとは
この木は Virtual Tree (虚树) とも呼ばれるらしい。
LCAをベースに構築する Auxiliary Tree は特定の頂点集合とそれらのLCAのみを含みながら、頂点同士の関係を失わないように木を圧縮したものである。
このAuxiliary Treeの頂点数は頂点集合のサイズに比べて高々2倍になり、構築は頂点集合のサイズの準線形オーダーの計算量で可能である。
そのため、木に対する多くのクエリが与えられるが、見るべき頂点の数がクエリ全体で限られている場合にこのテクニックを適用することができる。
例えば、以下のような問題が解ける。
Codeforces: 613D - Kingdom and its Cities
頂点からなる木が与えられ、個のクエリが与えられる。
番目のクエリでは個の頂点が与えられるので、この個の頂点同士を非連結にするために(個以外の頂点で)削除するべき頂点の最小個数を答えよ。
(不可能な場合は"-1")
- 制約
この問題は各クエリごとにDPをすると解くことができるが、各クエリごとに木全体を探索しながら計算すると となり間に合わない。
それに対しLCAベースのAuxiliary Treeを使うことで、各クエリを で計算でき、全体で前計算含め で計算できるため間に合うようになる。
構築方法
与えられた頂点集合に対し、LCAベースのAuxiliary Treeは大雑把に以下の流れで構築できる。
具体的な方法を調べてたら以下の構築方法があったので、これらの方法を書いておく。
- ソート1回、depthの関係からstackからpopする方法
- ソート2回、Euler Tour上の頂点の位置関係からstackからpopする方法
ソート1回の方法
1回ソートしたリストについて、 から と のLCAを求めながら、Auxiliary Treeを構築していく方法。
具体的な流れは以下のようになる。
- はじめに空のstackに をpushし、 から処理を行う。
- まで処理した後に を処理する場合、以下のようにstackを更新する。 (この時点のstackのtopは になる)
- と の LCA を求める
- stackのtopの頂点 について を満たす間 pop を繰り返す
- 頂点 と 頂点 を stack に push
各頂点の親頂点は、stackにpushされた後、popされた時点での親頂点(stackの次のtop もしくは 頂点)になる。
また、popされずに頂点まで処理を完了した時点でstackに残っている頂点の親頂点は、その時点のstackの並びから分かる。
ここで具体例を考える。
以下のような木を考え、頂点2, 6, 9, 12を含むAuxiliary Treeを構築するとする。
ここでは、頂点2, 6, 9まで計算し終わったとする。この時、stackはtopの方から頂点9, 頂点5, 頂点1が含まれる。
その次の頂点である頂点12を処理する。まず頂点9と頂点12のLCAを計算する(このLCAは頂点3である)。
次に、頂点3より深い頂点を全てpopする。ここでは、頂点9と頂点5がpopされる。(この時点で頂点9の親頂点が頂点5、頂点5の親頂点が頂点3であることが分かる)
頂点3と頂点12を順番にstackにpushし、頂点12までの処理を完了する。(この時点で頂点2, 6, 9, 12の全ての頂点を処理したので、stack上の頂点3と頂点12の親頂点も確定する)
ソート2回の方法
この方法では、1回ソートした後に と の各LCAを一旦求める。その後、頂点集合と求めたLCAを含めてもう一度ソートし、このソートされたリストを元にAuxiliary Treeを構築していく。
ここでは、2回ソートした後のリストを (重複は除く、)とする。
例えば、さっきの例と同じように頂点2, 6, 9, 12を含むAuxiliary Treeを構築する場合、2回ソートを行ったリスト(頂点1, 2, 3, 5, 6, 9, 12)を元に構築していく
次に、stackから頂点をpopする際の判定方法であるが、 と のLCAを求めてdepthをチェックしてもよいが、木上のEuler Tourにおける頂点の出現位置の関係を見ることでも判定できる。
具体的には、ETにおける頂点の最初の出現位置を、最後の出現位置をとする時、stackのtopの頂点と現在処理している頂点が を満たす(= 根頂点から頂点のパス間に(今まで見ていた)頂点が存在しない)限りpopすればよい。
下の図は、頂点9まで処理した後に頂点12を処理しようとする例であり、stackには頂点9, 5, 3, 1がtopから順に並んでいる。
下に示すET上では、を満たすことから、頂点9,5は根頂点と頂点12の間のパス上にないことが分かるため、stackからpopする。(頂点1, 3はを満たすのでstackに残しておく)
流れをまとめると、以下のように処理していく。
- はじめに空のstackに をpushし、 から処理を行う。
- まで処理した後に を処理する場合、以下のようにstackを更新する。 (この時点のstackのtopは になる)
- stackのtopの頂点 について を満たす間 pop を繰り返す
- 頂点 を stack に push
各頂点の親頂点は、stackにpushされる直前のstackのtopの頂点になる。
親頂点が決定しやすいという点でいえば、2回ソートの方が構築しやすい気がする。
実装
1回ソート、2回ソートの両実装を書いた。
- ETT + Sparse Table を用いた実装
- 実装は Python (C++実装は2回ソートの方を実装メモの方に載せてある)
- 各クエリを処理した後にAuxiliary Treeの隣接リストを初期化する必要がないように実装してある
前計算
- Euler Tour Technique における計算:
- Sparse Table の前計算:
# FS[v]: ET上の頂点vの最初の出現位置 # LS[v]: ET上の頂点vの最後の出現位置 # depth[v]: 頂点vの深さ FS = LS = depth = None # lg[x]: log_2 x の値 lg = None # st[lv][v]: Sparse Table st = None # 前計算 O(N log N) # - LCAの前計算を行う def construct(N, G): global FS, LS, depth, G0, lg, st # S: グラフG の Euler Tour S = [] FS = [0]*N; LS = [0]*N depth = [0]*N # Euler Tour Technique def dfs(v, p, d): depth[v] = d FS[v] = len(S) S.append(v) for w in G[v]: if w == p: continue dfs(w, v, d+1) S.append(v) LS[v] = len(S) S.append(v) dfs(0, -1, 0) # Sparse Table の構築 L = len(S) lg = [0]*(L+1) for i in range(2, L+1): lg[i] = lg[i >> 1] + 1 st = [[L]*(L - (1 << i) + 1) for i in range(lg[L]+1)] st[0][:] = S b = 1 for i in range(lg[L]): st0 = st[i] st1 = st[i+1] for j in range(L - (b<<1) + 1): st1[j] = (st0[j] if depth[st0[j]] <= depth[st0[j+b]] else st0[j+b]) b <<= 1
Auxiliary Treeの構築 (ソート1回)
- 計算量は
# LCAを求める O(1) def lca(u, v): # 常に x < y を満たすため、 x > y の時のswap処理は入れてない x = FS[u]; y = FS[v] l = lg[y - x + 1] return st[l][x] if depth[st[l][x]] <= depth[st[l][y - (1 << l) + 1]] else st[l][y - (1 << l) + 1] # Auxiliary Treeの構築 O(k log k) # リストvsで与えられたk個の頂点からAuxiliary Treeを構築 # # - 構築結果は G0 に入る # - 計算後のリストvsにはAuxiliary Treeの頂点が全て含まれる # - 返り値はAuxiliary Tree上の根頂点 def auxiliary_tree(k, vs, G0): vs.sort(key=FS.__getitem__) stk = [vs[0]] G0[vs[0]] = [] # LCAを求めながら Auxiliary Tree を構築 for i in range(k-1): w = lca(vs[i], vs[i+1]) # vs[i] が vs[i+1] の祖先でない場合はstackからのpopを行う if w != vs[i]: # 頂点wより深い頂点を全て pop する # last: stackの最もtopの頂点を表す last = stk.pop() while stk and depth[w] < depth[stk[-1]]: # lastの頂点の親頂点が決まる G0[stk[-1]].append(last) # popすることでlastも更新 last = stk.pop() # 頂点w の直下の頂点(last)を子頂点として決定 if not stk or stk[-1] != w: stk.append(w) vs.append(w) G0[w] = [last] else: G0[w].append(last) stk.append(vs[i+1]) G0[vs[i+1]] = [] # stack上に残った頂点も親子関係を決定する for i in range(len(stk)-1): G0[stk[i]].append(stk[i+1]) return stk[0]
Auxiliary Treeの構築 (ソート2回)
- 計算量は
# LCAを求める O(1) def lca(u, v): x = FS[u]; y = FS[v] l = lg[y - x + 1] return st[l][x] if depth[st[l][x]] <= depth[st[l][y - (1 << l) + 1]] else st[l][y - (1 << l) + 1] # Auxiliary Treeを構築 O(K log K) # リストvsで与えられたk個の頂点からAuxiliary Treeを構築 # # - 構築結果は G0 に入る # - 計算後のリストvsにはAuxiliary Treeの頂点が全て含まれる (重複あり, ソート済み) # - 返り値はAuxiliary Tree上の根頂点 def auxiliary_tree(k, vs, G0): # 頂点をpre-orderの順序でソート vs.sort(key=FS.__getitem__) for i in range(k-1): # vs[i], vs[i+1] の LCA w を vs に追加 vs.append(lca(vs[i], vs[i+1])) # LCAを追加した後にもう一度pre-orderの順序でソート vs.sort(key=FS.__getitem__) # Auxiliary Tree を構築 stk = [] prv = -1 for v in vs: if v == prv: # 重複は処理しない continue # ET上で last(stk[-1]) < first(v) を満たす間stackから pop していく while stk and LS[stk[-1]] < FS[v]: stk.pop() # 頂点vの親頂点は、頂点vをstackにpushする時点で決定される if stk: G0[stk[-1]].append(v) G0[v] = [] stk.append(v) prv = v return stk[0]
C++版の実装はこちら
tjkendev.github.io