Bottom View of Binary Tree
配列 root は二分木を保持しています。ルートはインデックス 0 にあり、インデックス i の子は 2i+1(左)と 2i+2(右)です。空の位置には -1 が入り、末尾に余分な -1 が含まれる場合もあります。
ルートは列 0 にあります。左へ進むと列が1つ左に移動し、右へ進むと列が1つ右に移動します。下から見ると、各列では最も深いノードが見えます。同じ深さに複数ある場合は、左から右へ読んだときに最後に来るノード(インデックスが大きい方)が見えます。見える値を、最も左の列から順に返してください。
関数
- rootinteger-array
- ヒープ順で表した木(空の位置は -1)
- 戻り値integer-array
- 各列を下から見た値。左端の列から順に。
制約
1 ≤ root.length ≤ 32767- 各
root[i]は-1、または0 ≤ root[i] ≤ 104を満たす値であり、同じ値は2回以上現れません。 root[0]は決して-1ではないため、木には少なくとも1つのノードがあります。- 配列は最後のノードの後に余分な
-1のエントリが続く場合があります。 - 空の位置の子は両方とも空であり、木のレベル数は最大でも
14です。
例
- 入力
- root = [5, 2, 9, 4, 7, 1, 6]
- 出力
- [4, 2, 1, 9, 6]
- 説明
- 列 0 には
5があり、2 レベル下には7(インデックス 4)と1(インデックス 5)があります。共有レベルでは1のほうが後にあるため、表示されます。列 -2、-1、1、2 にはそれぞれ4、2、9、6があります。
- 入力
- root = [1, 2, 3, -1, 4, -1, -1, -1, -1, 5]
- 出力
- [5, 4, 3]
- 説明
- 列 -1 には
2があり、2 段下には5(インデックス 9)があるため、5が見えます。列 0 では、ルートの下に4があります。列 1 には3だけがあります。
- 入力
- root = [8, -1, -1, -1]
- 出力
- [8]
- 説明
- ルートの後のすべての位置は
-1なので、8が唯一のノードで、列0が唯一の列です。
発展問題
ある列の最深レベルで同点の場合に、同点のすべてのノードを左から右の順に返すには、幅優先版をどのように変更すればよいでしょうか?例1の列0は何を返すでしょうか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
列に番号を付けます。ルートは0で、左に1歩進むと1を引き、右に1歩進むと1を足します。下から見たとき、ある列のどのノードが見えますか。また、その列の2つのノードが同じ高さにある場合はどうなりますか。
ノードをレベルごとに訪問し、各レベル内では左から右へ進み、各ノードの値をその列に書き込んで、そこにあった値を置き換えます。最後に各列に残っている値は何ですか?
(インデックス, 列) のペアのキューを使い、左の子を右の子より先にキューに追加します。各ノードについて列の値を上書きし、列の最小値と最大値を記録して、最後に列を順番に読み取ります。
解説
各列には最も深いノードが表示され、同じ深さの場合はレベル順で後にあるノードが選ばれます。幅優先探索ではノードをレベル順に訪問するため、各列で最後に訪問するノードが答えになります。各ノードで、その列のスロットを上書きしましょう。
深さ優先探索で、各列の最も深いノード
考え方
ツリーを深さ優先で走査し、各ノードに列と深さを割り当て、列ごとに、これまでで最も深いノードの深さと値を保持します。
左部分木を右部分木より先に訪問すると、同じレベルのノードを左から右へたどることになるため、求めるノードは最後に訪問したノードです。深さが以上の場合に置き換えます。例1では、深さ2、列0にある7、続いて1を訪問し、列には1が採用されます。
列は途切れのない範囲を形成するため、最小値と最大値を追跡し、その間のすべての列を読み取ります。各ノードを1回訪問するので、時間計算量はO(n)で、再帰はhレベルのツリーに対して深さhの呼び出しになります。
アルゴリズム
- 列 0、深さ 0 でルートを訪問します。
- 配列の末尾を超えている場合、または
root[i]が-1の場合は、インデックスiをスキップします。 - その列にまだノードがない場合、またはこの深さが保存されている深さ以上の場合は、この深さと値を保存します。
- 列
col-1で2i+1を訪問し、次に列col+1で2i+2を訪問します。どちらも1段深い位置です。 - 保存された値を、列の小さい順に返します。
def bottomView(root):
n = len(root)
best = {} # column -> (depth, value) of the deepest node found so far
def visit(i, col, depth):
if i >= n or root[i] == -1:
return
# Replace on a greater or equal depth: on a tie, the node
# met last is the one further right, which comes later in level order.
if col not in best or depth >= best[col][0]:
best[col] = (depth, root[i])
visit(2 * i + 1, col - 1, depth + 1)
visit(2 * i + 2, col + 1, depth + 1)
visit(0, 0, 0)
# The columns that hold a node form one unbroken range.
return [best[col][1] for col in range(min(best), max(best) + 1)]幅優先探索、各列の最後のノード
考え方
各ノードが取り出されるたびに左の子、続いて右の子を追加するキューは、ノードをレベル順に取り出します。つまり、深さ d のすべてのノードを深さ d+1 のノードより先に、各レベルでは左から右に処理します。同じ列のノードでは、最も深いノードが最後に取り出され、深さが同じなら右側のノードが後になります。そのため、各ノードが自分の列のスロットを上書きすると、各スロットには下から見えるノードが残り、深さを比較する必要はありません。
例1では、7 が列0に書き込み、1 がそれを上書きします。
各ノードにつきキューからの取り出しとマップへの書き込みが1回ずつ必要です。時間計算量は O(n) です。キューにはレベル全体、つまりノードのおよそ半分が入ることがあるため、空間計算量は O(n) です。
アルゴリズム
- ペア (index 0, column 0) をキューに入れます。
- ペア (
i,col) を取り出し、root[i]をcolの位置に格納し、以前の値があれば置き換えます。 - 配列の範囲内にあり、
-1ではない各子について、(2i+1,col-1)、次に (2i+2,col+1) を追加します。 - キューが空になるまで繰り返し、最小列と最大列を追跡します。
- 最小の列から順に、マップの値を返します。
from collections import deque
def bottomView(root):
n = len(root)
bottom = {} # column -> value of the last node seen in that column
queue = deque([(0, 0)]) # (node index, column); the root is never empty
while queue:
i, col = queue.popleft()
# The queue hands out deeper nodes later, and right after left
# on one level, so the last node to reach a column is the one seen from below.
bottom[col] = root[i]
left, right = 2 * i + 1, 2 * i + 2
if left < n and root[left] != -1:
queue.append((left, col - 1))
if right < n and root[right] != -1:
queue.append((right, col + 1))
# The columns that hold a node form one unbroken range.
return [bottom[col] for col in range(min(bottom), max(bottom) + 1)]
落とし穴と境界ケース
- 深さ優先版で、深さが厳密に大きい場合にのみ置き換えること。例1では、
1ではなく7が保持されます。 - 幅優先探索で各列の最初のノードを保持すること。これが上面図です。
- 左の子より先に右の子をキューに追加すること。各レベルが右から左に並び、同じ深さの場合に誤ったノードが選ばれます。
- 葉を答えとすること。例1では葉の
7が1に隠され、例2ではノード4に子があるにもかかわらず、列0の最下部にあります。 - 列をテキストとしてソートすること。文字列として扱うと、
"-10"は"-1"と"-2"の間に位置します。
よくある質問3
二分木の底面図とは何ですか?
ルートに列0を割り当て、左への各ステップでは列を1つ減らし、右への各ステップでは1つ増やします。下から見た表示では、左から右へ各列で最も深いノードを並べます。この問題では、深さが同じ場合はレベル順で判定し、後のノードを残します。
二分木の底面図の時間計算量はどれくらいですか?
n 個のノードに対して O(n):幅優先探索では、各ノードを1回処理し、ハッシュマップへの書き込みを1回行います。ツリーマップなどの順序付きマップでは、w 個の列に対して log w の係数が加わります。
上面図と下面図の違いは何ですか?
どちらも各列から1つのノードを選びます。上面図では最も浅いノードを選び、下面図では最も深いノードを選びます。幅優先版では、上面図は各列が最初に受け取った値を保持し、下面図は値を受け取るたびに上書きします。
def bottomView(root):
# ここにコードを書いてくださいテスト #1
テスト #2
テスト #3
引数
root = [5, 2, 9, 4, 7, 1, 6]
期待される出力
[4, 2, 1, 9, 6]