Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions 112_PathSum/solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# 問題
https://leetcode.com/problems/path-sum/

二分木の root と整数 targetSum を受け取る。root から葉(leaf)までのパスに沿ったノードの値の合計が targetSum に一致するパスが存在するかを bool で返す。

- 例1: root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 → true(5→4→11→2 の合計が22)
- 例2: root = [1,2,3], targetSum = 5 → false
- 例3: root = [], targetSum = 0 → false
- 制約: ノード数は [0, 5000]、-1000 <= Node.val <= 1000、-1000 <= targetSum <= 1000

# 前提
- 答えを見ずに考えて、5分考えて分からなかったら答えを見てください。答えを見て理解したと思ったら、答えを隠して書いてください。筆が進まず5分迷ったら答えを見てください。そして、見ちゃったら一回全部消してやり直しです。答えを送信して、正解になったら、まずは一段階目です。
- 次にコードを読みやすくするようにできるだけ整えましょう。これで動くコードになったら二段階目です。
- そしたらまた全部消しましょう。今度は、時間を測りながら、もう一回、書きましょう。書いてアクセプトされたら文字を消してもう一回書きましょう。これを10分以内に一回もエラーを出さずに書ける状態になるまで続けてください。3回続けてそれができたらその問題はひとまず丸です。


# 1回目
```go
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
// 方針: 一個見つければ終了してよさそうなので,DFSでもBFSでもよさそうだ.ただし,leafに行った時点での判定なので,DFSのほうが直感的に思える
// 注意すべきはexample3で,treeがemptyだと一致ではないという判定をする必要がある
// DFSで,その時点までのpathSumを引数として渡していく.leafが自分自身と足し合わせた時にtargetSumと一致していたらtrueを返す.
// 入力の長さは 0 <= N <= 5 * 10^3
// 最悪ケースですべてのノードを辿ったとする,かつGoの秒間ステップ数を10^8とすると
// 5 * 10^-5 なので,0.05 ms程度
// メモリは,最悪ケースで5 * 10^3回の再帰.引数は,root(ポインタ),targetSum(int), pathSum(int)とすると,8*3 = 24byte, リターンアドレスで+8B, ローカル変数はあとで考慮するとして,ここまでで(32+x) * 5 * 10^3 = 160KB+5xKB程度. goroutineの1GB上限には達しなさそうだ.

func hasPathSum(root *TreeNode, targetSum int) bool {
return hasPathSumFunc(root, targetSum, 0)
}

func hasPathSumFunc(root *TreeNode, targetSum, pathSum int) bool {
if root == nil {
return false
}
// 停止条件
if root.Left == nil && root.Right == nil {
return root.Val + pathSum == targetSum
}
pathSum += root.Val
left := hasPathSumFunc(root.Left, targetSum, pathSum)
right := hasPathSumFunc(root.Right, targetSum, pathSum)
return left || right
}

@h-masder h-masder Jul 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conditional ORが短絡評価をしてくれるので、それを使うことで、(全部見ることなく)発見したタイミングでreturn できます。

https://go.dev/ref/spec?utm_source=chatgpt.com#Logical_operators

func hasPathSumFunc(root *TreeNode, targetSum, pathSum int) bool {
    if root == nil {
        return false
    }
    // 停止条件
    if root.Left == nil && root.Right == nil {
        return root.Val + pathSum == targetSum
    }
    pathSum += root.Val
    return hasPathSumFunc(root.Left, targetSum, pathSum) || hasPathSumFunc(root.Right, targetSum, pathSum)
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

たしかに.自分のコードだと短絡評価を活用できていないですね.
leetcodeの判定が0msだったので,ちょっと満足してしまっていたかもしれないですね.

コメントありがとうございます

```
- これでpassed.
- セルフレビュー
- hasPathSumFuncの命名はどうにかしたい
- そのなかでpathSumを上書きするのもちょっと嫌
- 再帰で書いてあるから,やっぱスタックオーバーフローは気になる.ループで書き直したい
- これはトップダウンの感じ.これまでの処理結果を上から渡すので.
- targetSumとpathSumを二つ渡していく必要はあるんだろうか?
- これはあれか,差をとってそれを渡していって,leaf時点で,0になればいいのか.すこしだけシンプルにできそうだ.
- 他の選択肢に思いを馳せる
- BFSもまああり.イメージとしては,複数チームに同時に指令をだして,最初に条件に一致したものが見つかった時点で打ち止めする,ような感じ
- ブルートフォースは,特に思い浮かばない.DFSも全部たどるので愚直解みたいなモノだしな.

# 2回目
```go
func hasPathSum(root *TreeNode, targetSum int) bool {
if root == nil {
return false
}
// 停止条件
if root.Left == nil && root.Right == nil {
return targetSum - root.Val == 0
}
nextTargetSum := targetSum - root.Val
left := hasPathSum(root.Left, nextTargetSum)
right := hasPathSum(root.Right, nextTargetSum)
return left || right
}
```

# 2回目 ループ
```go
func hasPathSum(root *TreeNode, targetSum int) bool {
type nodeAndPathSum struct {
node *TreeNode
pathSum int
}
toVisit := []nodeAndPathSum{
{node: root, pathSum: 0},
}

found := false
for len(toVisit) > 0 {
v := toVisit[len(toVisit)-1]
toVisit = toVisit[:len(toVisit)-1]
node, pathSum := v.node, v.pathSum

if node == nil {
continue
}
nextPathSum := pathSum + node.Val
if node.Left == nil && node.Right == nil {
if nextPathSum == targetSum {
found = true
break
}
}
toVisit = append(toVisit, nodeAndPathSum{node: node.Left, pathSum: nextPathSum})
toVisit = append(toVisit, nodeAndPathSum{node: node.Right, pathSum: nextPathSum})
}
return found
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

関数名がhasPathSumなので、true/falseを返していいのかなと思いました。
また、breakせずともその場でreturnしてもいいのかなと思いました。

    for len(toVisit) > 0 {
        v := toVisit[len(toVisit)-1]
        toVisit = toVisit[:len(toVisit)-1]
        node, pathSum := v.node, v.pathSum

        if node == nil {
            continue
        }
        nextPathSum := pathSum + node.Val
        if node.Left == nil && node.Right == nil {
            if nextPathSum == targetSum {
                return true
            }
        }
        toVisit = append(toVisit, nodeAndPathSum{node: node.Left, pathSum: nextPathSum})
        toVisit = append(toVisit, nodeAndPathSum{node: node.Right, pathSum: nextPathSum})
    }
    return false
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

関数名がhasPathSumなので、true/falseを返していいのかなと思いました。

これはそれもアリだなと思います.
書いた時にはreturnする場所はあまり増やしたくないなと感じてました.
一方で普段early returnも有用だと考えてるので,なんか個人的に使い分けをもうちょい明確にしたい部分だなと気づきました.

コメントありがとうございます

```
- 問題なく通った.
- 他の人のコードをみにいく
- https://github.com/chryschron/codings/pull/24
- step1に対して,if (codition) { return true } としている箇所がすこし気になる.それが2回繰り返されているのも.以下のように書きたい気持ち. syntaxには自信はない.
```cpp
bool left = hasPathSum(root->left, target_sum - root->val)
bool right = hasPathSum(root->right, target_sum - root->val)
return left || right
```
- 引数に渡すときにはもう足しておくこともたしかにできるのか.ちょっとずれる感じがして好みではないが了解,という感じ
- step3は綺麗で読みやすい
- https://github.com/jjysogfy/arai60-202603/pull/15
- step1から綺麗.これは引きながら渡していくスタイル.
- 事前に引いていくスタイルも二度目だと,まぁたしかに自然に思えてきた
- step2のレコードは,nodeとnum, numという命名はもうすこし改善の余地ありそうな気もする.`rest` とか?うーむ.
- https://github.com/Hiroto-Iizuka/coding_practice/pull/25
- pythonのtupleはイミュータブルなんだという学び
- remainingという命名はいいですね
- と思ったらstep2でprefix_sumになってた.これはこれでよさそう
- step3はないのかな
- https://github.com/hiroki-horiguchi-dev/leetcode/pull/25
- recordの定義,先にしてくれたほうが個人的には嬉しいな.慣習的にはどっちなんでしょうね
- 終了判定をする前に,一度queueに詰めるのが先なのが,なんとなく自分と違うなと思った
- `NodeAndPrefixSum` のlistが,nodesという変数名なのはすこし気になる.名前とずれている気がして.
- https://github.com/nicah4o/arai60/pull/24
- `return left_value || right_value` のほうが自然というのは同意
- 47,48行目のifには`{}`がほしい
- この人に限らないんだが,計算量O(N)というのはどう考えた結果O(N)としたのかは,気になる
- https://github.com/dorxyxki/arai60/pull/25
- 読みやすい
- あまりpythonの作法を知らないんだが,以下のようなのはよくあることなのだろうか.ちょっとだけ面食らう
```python
def xxx
def yyy

```
- https://github.com/attractal/leetcode/pull/37
- `if root is None`のほうがよいかもしれない
- if xxx and not yyy はちょっと負荷高まった気がする.自分は避ける.
- https://github.com/hiro111208/leetcode/pull/19
- どんどんネストが深くなるなぁ,ちょっと下げたいなぁの気持ち
- https://github.com/h-masder/Arai60/pull/28
- [2]の解法は,layer分けをしているんだなと思ったけど,今回の問題はする必要がなさそうと思った
- 再帰にするとき,関数名困るけど,helperというのはよいかわしかただと思った
- フォローアップにも取り組んでいて素晴らしいなと思った
- https://github.com/rimokem/arai60/pull/25
- 感じることは全部コメントされていますね
- https://github.com/Manato110/LeetCode-arai60/pull/25
- いい感じだ.2分は速い
- 空間計算量はO(N)
- 同上で済ませられているけど,本当だろうか?ループの度にpopされるのでそこまでいかない気もするが勘違いかな.あるいはpopしたとて定数倍だから無視できるぽいかな

# 3回目
```go
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func hasPathSum(root *TreeNode, targetSum int) bool {
type nodeAndRemaining struct {
node *TreeNode
remaining int
}

toVisit := []nodeAndRemaining{{node: root, remaining: targetSum}}

found := false
for len(toVisit) > 0 {
v := toVisit[len(toVisit)-1]
toVisit = toVisit[:len(toVisit)-1]
node, remaining := v.node, v.remaining

if node == nil {
continue
}
if node.Left == nil && node.Right == nil {
if remaining == node.Val {
found = true
break
}
}
toVisit = append(toVisit, nodeAndRemaining{node: node.Left, remaining: remaining - node.Val})
toVisit = append(toVisit, nodeAndRemaining{node: node.Right, remaining: remaining - node.Val})
}
return found
}
```
- これを3回繰り返した

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

全体的に読みやすかったです。