再帰で二分木の中に別の木と同じ部分木があるか確かめる

大きい二分木のどこかの節から下が、小さい二分木とそっくり同じかを返します。どの節を根にするかを探す再帰と、2 本の木が同じかを比べる再帰を組み合わせる練習です。

海外で定番の問題リスト 4 本には入っていません

問題

2 本の二分木の根 root1 と root2 を受け取ります。root1 の木のどこかの節について、その節とその下の子孫を全部合わせた部分木が、root2 の木と形も値もそっくり同じなら true、そうでなければ false を返す関数を書いてください。

  • 部分木は、ある節から下を葉まで全部含めた物です。途中で切った形は部分木ではありません
  • root1 の木そのものも、root1 の部分木の 1 つです
  • 各節は val (値)、left (左の子)、right (右の子) を持ちます。子が無いところは、Python では None、JavaScript では null、Go では nil です。Go の節は TreeNode で、Val、Left、Right と大文字で始まります
  • root1 の節の数は 1 以上 2000 以下、root2 の節の数は 1 以上 1000 以下、値はどれも -10000 以上 10000 以下です

例では、木を上の段から左の順に読んだ値の並びで書きます。子が無いところは null です。

入力と出力の例

例 1

根が 8、その下が 6 と 9、6 の下に 2 と 7 の木です。root1 の 6 の節から下は、6 の左に 2、右に 7 で、root2 とそっくり同じです。

root1 = [8, 6, 9, 2, 7] root2 = [6, 2, 7]
true

例 2

root1 の 6 の節から下は、7 の左にさらに 5 が付いています。葉まで全部比べると root2 と違うので、部分木ではありません。

root1 = [8, 6, 9, 2, 7, null, null, null, null, 5] root2 = [6, 2, 7]
false

例 3

値が同じでも、左と右が違えば同じ形ではありません。root1 の 2 の節から下は 2 の左に 7 ですが、root2 は 2 の右に 7 です。

root1 = [1, 2, 3, 7] root2 = [2, null, 7]
false

面接での聞かれ方

木の再帰を 2 つ組み合わせる問題として、ジュニアの面接で出ます。

  • 「この木の中に、もう 1 本の木と同じ部分木があるかを調べてください」
  • 2 本の木が同じかを調べる問題 (この一覧の same-tree) の続きとして出されることがよくあります
  • 書き終えたあとに「もっと速くできますか?」と聞かれることがあります。木を区切りの付いた文字列に直して文字列の中を探す書き方や、部分木ごとに指紋 (ハッシュ) を付ける書き方で、O(m + n) にできます

ジュニアの問題なので、「どこを根にするか」と「同じか」の 2 つの関数に分けて考えられるかが見られます。

ヒント

まだ開いていません

付箋は 4 枚。下の段ほど答えに近づきます

  1. ヒント 2

  2. ヒント 3

  3. ヒント 4答えに近い

アルゴリズムのチートシートの「再帰と木」に、この型の要点を表とコードでまとめています。

再帰は、同じ形の小さい問題に分けて自分を呼ぶ書き方です。木の深さや、全部の組み合わせを数える問題で使います。

main.py
def subtree_check(root1: TreeNode | None, root2: TreeNode | None) -> bool:
    """
    Goal: 何を返すか

    idea1: まず思いつく案
    -> runtime: O(?)  space: O(?)

    idea2: もっと速い案
    -> runtime: O(?)  space: O(?)

    edge case: 空、1 つだけ、など
    """
    # root の節は val、left、right を持ちます。空きは None です
    # ここに書きます
    pass


# 「実行」を押すと、この行の結果が出ます
# build_tree は、例と同じ並びから木を組みます
print(subtree_check(build_tree([8, 6, 9, 2, 7]), build_tree([6, 2, 7])))

できているか「例で確かめる」を押すと、問題の例を全部この関数に渡して、通ったかをここに出します