再帰で二分木の道の値の合計がいちばん大きくなる所を探す

二分木の中で、節をつないだ道の値の合計のうち、いちばん大きい値を返します。負の値の枝を切り捨てながら、下へ伸ばせる片道の和を返し、ついでに曲がる道を比べる再帰の練習です。

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

問題

二分木の根 root を受け取ります。木の中の道のうち、道が通る節の値の合計がいちばん大きい物の、その合計を返す関数を書いてください。

  • 道は、親と子を結ぶ辺をたどって、同じ節を 2 回通らずにつないだ節の列です。節が 1 つだけでも道です
  • 道は根を通らなくてもかまいません。ある節で左の枝から上がってきて右の枝へ降りる、という曲がり方は 1 回だけできます
  • 値には負の数もあります。全部が負なら、答えはいちばん大きい 1 つの節の値です
  • 各節は val (値)、left (左の子)、right (右の子) を持ちます。子が無いところは、Python では None、JavaScript では null、Go では nil です。Go の節は TreeNode で、Val、Left、Right と大文字で始まります
  • 節の数は 1 以上 500 以下、値はどれも -1000 以上 1000 以下です

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

入力と出力の例

例 1

根が 2、その下が 6 と -4、6 の下に 5 と 1、-4 の右に 9 です。5 から 6、2 と上がって、-4、9 と下る道の合計は 5 + 6 + 2 − 4 + 9 = 18 で、これがいちばん大きくなります。

root = [2, 6, -4, 5, 1, null, 9]
18

例 2

根の -6 を通ると損をします。根を通る道は、いちばん大きくても 4 − 6 + 12 + 10 = 20 です。根を通らず、右の 12 で 10 から 12、3 と曲がる道の 25 がいちばん大きくなります。

root = [-6, 4, 12, null, null, 10, 3]
25

例 3

全部の値が負の木です。どの道も、節を足すほど小さくなるので、いちばん大きい 1 つの節の -2 が答えです。

root = [-3, -2, -5]
-2

面接での聞かれ方

木の再帰の難しい問題として、シニアの面接でよく出ます。

  • 「二分木の道の合計の最大を求めてください。道は根を通らなくてもよく、値には負の数もあります」
  • 二分木の直径を求める問題 (この一覧の tree-diameter) と同じ形で、辺の数の代わりに値を足し、負の値を捨てる判断が加わった物です
  • 「どの道が最大か、道そのものも返せますか?」と続けて聞かれることがあります

シニアの問題なので、「親へ返す値」と「その節で比べる値」が違うことを、言葉で説明できるかが見られます。

ヒント

まだ開いていません

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

  1. ヒント 2

  2. ヒント 3

  3. ヒント 4答えに近い

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

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

main.py
def max_path_sum(root: TreeNode | None) -> int:
    """
    Goal: 何を返すか

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

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

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


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

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