-
Notifications
You must be signed in to change notification settings - Fork 0
112. Path Sum #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
112. Path Sum #24
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| start_new_problem.sh | ||
| main.go | ||
| go.mod | ||
| go.sum | ||
| *.go |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| 以下のコードは全てGoのデフォルトフォーマッターgofmtにかけてあります。 | ||
|
|
||
| ```Go | ||
| // Definition for a binary tree node. | ||
| type TreeNode struct { | ||
| Val int | ||
| Left *TreeNode | ||
| Right *TreeNode | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 1 | ||
| - 気にした制約条件: 二分木はバランスされているかどうか | ||
| - 方針: 各ノードについて、根からそのノードまでのパスの値の合計値を保持する構造体を作り、 | ||
| DFSをして、葉ノードに到達したらパスの合計値がtargetSumと等しいか調べ、等しかったらtrueを返す。 | ||
| 等しくならないまま全探索を終えたらfalseを返す | ||
| - 実装: 実装はスムーズに進んだが、テストケースでnilポインタへのアクセスが生じてエラーが出た。 | ||
| 原因はスライス`stack`をmakeする際に`stack := make([]nodePathSum, 5000)`としてしまい、 | ||
| キャパシティの値を間違って要素数として設定してしまっていたこと。 | ||
| これによって、5000個のnil値が格納されてしまっていた。 | ||
| - nをノード数とすると、計算量は以下の通り | ||
| - 時間計算量: O(n) | ||
| - 空間計算量: O(n) | ||
| - あるノードでpathSumがtargetSumを超えたらそのノードの子は探索する必要がなくなる(stackに入れなくて良い)と思ったが、 | ||
| 制約条件でノードの値は負の数もあるのでこの最適化手法は使えない | ||
| - ノード数が最大で5000で、各ノードの値の最大値が1000なので、pathSumは最大5e6になりうる。 | ||
| 2^10 ≒ 1e3 より、5e6 < 2^22。Goのint型は32bitマシンで32bitなので、integer overflowは起きない。 | ||
|
|
||
| ```Go | ||
| type nodePathSum struct { | ||
| node *TreeNode | ||
| pathSum int | ||
| } | ||
|
|
||
| func hasPathSum(root *TreeNode, targetSum int) bool { | ||
| if root == nil { | ||
| return false | ||
| } | ||
| stack := make([]nodePathSum, 0, 5000) | ||
| stack = append(stack, nodePathSum{root, root.Val}) | ||
| for len(stack) > 0 { | ||
| top := stack[len(stack)-1] | ||
| stack = stack[:len(stack)-1] | ||
| if top.node.Left == nil && top.node.Right == nil { | ||
| if top.pathSum != targetSum { | ||
| continue | ||
| } | ||
| return true | ||
| } | ||
| if top.node.Left != nil { | ||
| stack = append(stack, nodePathSum{top.node.Left, top.pathSum + top.node.Left.Val}) | ||
| } | ||
| if top.node.Right != nil { | ||
| stack = append(stack, nodePathSum{top.node.Right, top.pathSum + top.node.Right.Val}) | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 2 | ||
| #### 2a 再帰 | ||
| - 再帰による回答 | ||
| - 再帰の深さは高々5000で、1スタックフレームの大きさを100Bとして考えると、500KBのスタックサイズが必要。 | ||
| Goは1GBで1e7回再帰できるので大丈夫。 | ||
| - 考え方: 再帰は役割分担の仕様書を考えるようにやると良いと以前どこかでodaさんが言っていた。 | ||
| 以下、`hasPathSumRecursive`の仕様書 | ||
| 1. 入力として現在地node、targetSum(不変)、現在地nodeの前までのパスの合計値、を取る | ||
| 2. 現在地nodeがnilだったらfalseを返す | ||
| 3. 現在地nodeが葉ノードだったら、パスの合計値がtargetSumと等しいかどうかを返す | ||
| 4. それ以外の場合は、まず左側のノードに処理をさせる。 | ||
| どこかで葉ノードが見つかったら3の結果が返ってくる。 | ||
| trueだったらtrueを親ノードに渡す(return値trueとして関数から出ようとする)。 | ||
| falseだったら右側のノードに処理を渡す。 | ||
| 同様に、どこかで葉ノードが見つかったら3の結果が返ってくるので、自身もその結果を返す。 | ||
| - なんか複雑な仕様書になった気がする。よりシンプルな解法はないだろうか? | ||
|
|
||
| ```Go | ||
| func hasPathSum(root *TreeNode, targetSum int) bool { | ||
| return hasPathSumRecursive(root, targetSum, 0) | ||
| } | ||
|
|
||
| func hasPathSumRecursive(node *TreeNode, targetSum int, pathSumBeforeNode int) bool { | ||
| if node == nil { | ||
| return false | ||
| } | ||
| pathSum := pathSumBeforeNode + node.Val | ||
| if node.Left == nil && node.Right == nil { | ||
| return pathSum == targetSum | ||
| } | ||
| found := hasPathSumRecursive(node.Left, targetSum, pathSum) | ||
| if found { | ||
| return true | ||
| } | ||
| found = hasPathSumRecursive(node.Right, targetSum, pathSum) | ||
| return found | ||
| } | ||
| ``` | ||
|
|
||
| #### 2b 再帰改良版 | ||
| - https://github.com/hayashi-ay/leetcode/pull/30/files | ||
| でシンプルな再帰の解答を発見 | ||
| - targetSumを減らしていくという発想ができなかった。 | ||
| これは再帰を書く時の基本的な発想の一つだという気がするので自分で思いつけるようになりたい | ||
| - あとは2aの最後の5行程度をorを使うことにより簡略化できることに気が付かなかった | ||
| - こちらの再帰も仕様書を書いてみる | ||
| 1. 上から仕事が回ってきた時、入力rootがnilだったらfalseを返す。 | ||
| 葉ノードだったらroot.ValがtargetSumと等しいかどうかを返す | ||
| 2. 1以外の場合、targetSumを更新して左ノードと右ノードにそれぞれ仕事を送る | ||
| 3. 下から仕事の結果が返ってきたら上にそのまま届ける | ||
|
|
||
| ```Go | ||
| func hasPathSum(root *TreeNode, targetSum int) bool { | ||
| if root == nil { | ||
| return false | ||
| } | ||
| if root.Left == nil && root.Right == nil { | ||
| return root.Val == targetSum | ||
| } | ||
| remaining := targetSum - root.Val | ||
| leftFound := hasPathSum(root.Left, remaining) | ||
| rightFound := hasPathSum(root.Right, remaining) | ||
| return leftFound || rightFound | ||
| } | ||
| ``` | ||
|
|
||
| #### 2c スタックDFS nilノードもスタックに入れる | ||
| - スタックDFSでnilノードもスタックに入れる方法。 | ||
| これにより、条件分岐が少なくなる。 | ||
| 一方、nilノードもスタックに入れるので、最悪の場合、一本のパスだけからなる二分木だとスタックの要素数が木の要素数の倍になってしまう | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. なるほど、この概算はできてなかったです。勉強になりました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 速度は実験してみるといいと思います。分岐予測の失敗もそこそこコストがかかります。 そもそも論として、速度にこだわる必要があるかは状況次第です。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nittoco There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 勝手に自分で文を「スタックに入った通算要素数」で解釈してしまってました。確かに要素数というと瞬間要素数を指すのが一般的かもしれないですね、ありがとうございます。 分岐予測は確かに頭になかったです。言語間の比較や、分岐予測も含めてコストを考えてみます。 |
||
|
|
||
| ```Go | ||
| type nodePathSum struct { | ||
| node *TreeNode | ||
| pathSumBeforeNode int | ||
| } | ||
|
|
||
| func hasPathSum(root *TreeNode, targetSum int) bool { | ||
| stack := make([]nodePathSum, 0, 10000) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. どうしてcapacityを指定しましたか?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. スライスに入りうる最大要素数を知っている場合にはキャパシティを指定した方が再割り当てが起きずパフォーマンスが向上することが多いので、キャパシティを指定しました ただ、今見返したら最大要素数は1e4ではなく5e3でした。木の要素数が最大で5e3で、木が右側への直線の時に、各ノードがnilノードも合わせて二つの子ノードを持ち、両方スタックに積まれるので 2 * 5e3 = 1e4個スタックに入ることになるかと思ったのですが、popされることを忘れていたので、5e3でした There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. capacityを大きく割り当てるとインプットのサイズが小さい時に悪影響がありそうでしょうか?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. はい、5e3個分のキャパシティを割り当てたのに実際の要素数が0だと5e3だけ無駄にメモリ使用量として数えられてしまうことになります。
というところまで吟味しました、というところまで書いておくべきでした There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. なるほど、まず、本当にそこの速度が大事なのかと全体が何秒速くなっているかを考えるか試してみるかして下さい。 次、メモリー使用量です、ポインターと int なので環境によりますがサイズは 8 + 4 バイトですか? たぶん、パディングされて16バイトでしょうか。それで、1万とっているので、これ、160キロバイトですかね。環境にもよりますがL1キャッシュのサイズを超えていそうです。ランダムアクセスではないのでかもしれませんが遅くなるかも。 最後に、10000という数がコードに現れると疑問が湧きます。「わざわざデフォルト引数から動かさなくてはいけなかったはずだ。どれくらいのコストを払ってこの数字を決めたのか、どれくらい動かしてはいけないのか、たとえば、1週間実験を繰り返した結果こうしたほうがいいと分かったのか、あるバグがあって特定条件で遅くなるのでこの数字にしておく必要があるのか。」コメントつけておいて欲しいですね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. メモリー確保などに分解して時間を測って、上の表と比較をすると面白いかもしれません。 「精進」は、2010年前後の JOI 勢がいい始めた気がしています。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://touyoubuntu.hatenablog.com/entry/20111216/1323992210
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. なんと、考察記事まであるとは。想像以上に界隈で使われていたようですね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2005年に ICPC の世界出場メンバーが私に向かって精進と書いているのを見つけました。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. その方が「精進」を根付かせた説はありますかね |
||
| stack = append(stack, nodePathSum{root, 0}) | ||
| for len(stack) > 0 { | ||
| top := stack[len(stack)-1] | ||
| stack = stack[:len(stack)-1] | ||
| node, pathSumBeforeNode := top.node, top.pathSumBeforeNode | ||
| if node == nil { | ||
| continue | ||
| } | ||
| pathSum := pathSumBeforeNode + node.Val | ||
| if node.Left == nil && node.Right == nil && pathSum == targetSum { | ||
| return true | ||
| } | ||
| stack = append(stack, nodePathSum{node.Left, pathSum}) | ||
| stack = append(stack, nodePathSum{node.Right, pathSum}) | ||
| } | ||
| return false | ||
| } | ||
| ``` | ||
|
|
||
| ### Step 3 | ||
| ```Go | ||
| func hasPathSum(root *TreeNode, targetSum int) bool { | ||
| if root == nil { | ||
| return false | ||
| } | ||
| if root.Left == nil && root.Right == nil { | ||
| return root.Val == targetSum | ||
| } | ||
| remaining := targetSum - root.Val | ||
| leftFound := hasPathSum(root.Left, remaining) | ||
| rightFound := hasPathSum(root.Right, remaining) | ||
| return leftFound || rightFound | ||
|
Comment on lines
+169
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return hasPathSum(root.Left, remaining) || hasPathSum(root.Right, remaining)とした方が短絡評価が起こるので良さそうです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 短絡評価という言葉を知らなかったです。ブール演算子による計算において、第一引数を評価して結果が定まらない場合のみ第二引数を評価するということですね |
||
| } | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
下のliquoriceさんのレビューでもありますが、短絡評価になるよう実装すればここの複雑なことをしなくても早めにreturnできますね。