Top View of Binary Tree
ヒープ順で配列 root に格納された二分木が与えられます。根はインデックス 0 にあり、インデックス i の子は 2*i+1(左)と 2*i+2(右)にあり、空の位置は -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, 5, 9, 6]
- 説明
- 左から右へ、列には
4(列 -2)、2(-1)、根の5(0)、9(1)、6(2)が入っています。7と1も列 0 にありますが、根はそれらの上にあります。
- 入力
- root = [1, 2, -1, -1, 3, -1, -1, -1, -1, -1, 4]
- 出力
- [2, 1, 4]
- 説明
2は列 -1 にあります。その右の子であるインデックス 4 の3はルートの下に戻って列 0 にあるため、表示されません。3の右の子であるインデックス 10 の4は、列 1 に単独であります。
- 入力
- root = [6, -1, -1]
- 出力
- [6]
- 説明
- インデックス1と2には
-1が入っているため、6は列0に単独で存在し、ビュー全体を構成します。
発展問題
配列の代わりにノードオブジェクトを使う場合、同点を決めるインデックスはありません。同点の場合のルールをどのように定めますか。また、幅優先探索のコードは変わりますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
列に番号を付けます。根は0で、左に進むと1を引き、右に進むと1を足します。上から見えるのはその列のどのノードですか。また、その列のほかのノードとどう区別できますか?
ノードをレベルごとに、各レベル内では左から右へとたどると、列のどのノードに最初に出会いますか?
(インデックス, 列)の組に対して幅優先探索を行い、左の子を右の子より先に追加します。列が初めて現れたときだけ値を保存し、列の最小値と最大値を追跡して、最後に列を順番に読み取ります。
解説
各列には最も浅いノードが示され、同じ深さの場合はレベル順で先に現れるノードが示されます。これは幅優先探索でノードを訪問する順序そのものなので、探索でその列の最初に見つかるノードが答えです。深さ優先探索でもできますが、深さを記録し、同じ深さの場合の優先順位に注意する必要があります。
深さ優先探索、各列で最も浅いノード
考え方
各ノードの列と深さを記録しながら木を深さ優先でたどり、列ごとに、これまでに見つかった最も浅いノードの深さと値を保持します。
深さが厳密に小さい場合にのみ置き換えます。同じ深さのノードが2つある場合、左部分木をすべて探索してから対応する右部分木を探索する走査では、レベル順で先に来るノードが先に見つかります。2つの経路が分岐する箇所で左に進むためです。同じ深さでも置き換えると、後のノードが優先されます。たとえば、インデックス3が空で、インデックス19と23にノードがある場合、どちらも深さ4の列-2に位置し、答えはインデックス19のノードです。
ノードが存在する列は途切れのない範囲になります。列-3のノードには列-1と-2の祖先がいるため、最小列と最大列を記録します。各ノードは1回ずつ訪問され、時間計算量は O(n) で、再帰の深さはおよそ h 回の呼び出しです。
アルゴリズム
- ルートを列 0、深さ 0 で訪問します。配列の範囲外、または
-1を保持するインデックスからは、ただちに戻ります。 - 列にまだノードがない場合、またはこの深さが保存されている深さより厳密に小さい場合は、この深さと値を保存します。
- 1 レベル深くして、
col-1の列にある2i+1を訪問し、次にcol+1の列にある2i+2を訪問します。 - 列の最小値から最大値の順に、保存された値を返します。
def topView(root):
n = len(root)
best = {} # column -> (depth, value) of the shallowest node found so far
def visit(i, col, depth):
if i >= n or root[i] == -1:
return
# Replace only on a strictly smaller depth: on a tie, the node
# met first is the one further left, which comes first 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回必要なので、全体で O(n) です。キューにはレベル全体を保持でき、ノードの半数近くまで入ることがあるため、空間計算量は O(n) です。
アルゴリズム
- ペア(インデックス 0、列 0)をキューに入れます。
- ペア(
i、col)を取り出します。マップにcolのエントリがなければ、そこにroot[i]を格納します。 - 存在する各子について、(
2i+1、col-1)、次に(2i+2、col+1)を追加します。 - キューが空になったら、最小の列から最大の列の順にマップの値を返します。
from collections import deque
def topView(root):
n = len(root)
top = {} # column -> value of the first 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 shallower nodes first, and left before right
# on one level, so the first node of a column is the one seen from above.
if col not in top:
top[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 [top[col] for col in range(min(top), max(top) + 1)]
落とし穴と境界ケース
- 深さ優先探索で最初に見つかったノードを採用する。根の左の子に右の子があり、その右の子にも右の子がある場合(列 1、深さ 3)、根の右の子(列 1、深さ 1)より先にそのノードに到達する。
- 右の子を先にキューに入れる。同順位の場合、誤ったノードが選ばれる。
- 列をテキストとして並べ替える。
"-1"は"-2"より前に並ぶ。 - 左部分木が負の列を埋めると思い込む。例 2 では、ノード
4は根の左部分木にあり、列 1 にある。
よくある質問3
二分木の上面図とは何ですか?
列にはルートから 0 を割り当て、左の子は 1 列左に、右の子は 1 列右にします。上面図には、左から右へ各列について、ルートに最も近いノードを列挙します。
二分木の上面図の時間計算量はどれくらいですか?
幅優先探索では、各nノードを1回ずつ訪問し、ノードごとにO(1)のマップ検索を行います。そのため、最小列と最大列を追跡すれば、実行時間はO(n)です。列を順序付きマップで管理する場合、w列に対してO(n log w)となります。
上面図は左側面図や底面図とどのように異なりますか?
左側ビューでは、各レベルの最初のノードを選びます。上面ビューでは、各列で最も浅いノードを選びます。下面ビューでは、列ごとに最も深いノードを選ぶため、その探索では各列の最初のノードを保持するのではなく、上書きします。
def topView(root):
# ここにコードを書いてくださいテスト #1
テスト #2
テスト #3
引数
root = [5, 2, 9, 4, 7, 1, 6]
期待される出力
[4, 2, 5, 9, 6]