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
123 changes: 123 additions & 0 deletions problem23/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
## 取り組み方
- step1: 5分以内に空で書いてAcceptedされるまで解く
- step2: discord内で他の方のコードを5人分読む + 何をよい/悪いと思ったか、何を避けたいと思ったか等感じたことを3つ以上かく
- step3: 10分以内に1回もエラーを出さずに3回連続で解く

## step1
### 考えたこと
再帰で書くのがシンプルで良さそう。
「再帰の終了条件->再帰のメイン処理->次の再帰の呼び出し->結果の返却」の流れで考える。

再帰の終了条件は、root1,root2がどちらもnullの時、もしくは、どちらか一方がnullの時で、それぞれnullとどちらか一方のrootを返せばよい。
再帰のメイン処理では、merged treeの現在のvalが何なのかを計算する。
次の再帰の呼び出しは、現在のmergedの頂点からleftとrightがどうなっているかを探索する。
結果の返却として、mergedを返す。

練習のため、DFSの再帰の実装とスタックの実装それぞれで解く。

##### 再帰
```python
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not root1 and not root2:
return None
if not root1:
return root2
if not root2:
return root1
merged = TreeNode(root1.val + root2.val)
merged.left = self.mergeTrees(root1.left, root2.left)
merged.right = self.mergeTrees(root1.right, root2.right)
return merged
```

##### stack
```python
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not root1 and not root2:
return None
if not root1:
return root2
if not root2:
return root1
Comment on lines +40 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この場合、そのまま下に流せば、ノードが入力と出力で共有されませんね。

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.

deepcopyで返す or 下に流さないと、mergeTreesの返り値を操作したときに入力を書き換えてしまう可能性があるんですね。。見逃していました。

shining-ai/leetcode#23 (comment)

root2をルートとするツリーについては、引数で渡ってきたものをそのまま使用しているので、この書き方だとmergeTreesの返り値のTreeにroot2の一部が使われてしまいます。この関数自体では非破壊的かもしれないですが、mergeTreesの返り値のTreeの操作を行うとroot2のツリーを書き換えてしまう可能性があります。


merged = TreeNode()
stack = [(root1, root2, merged)]
while stack:
node1, node2, merged_node = stack.pop()

if node1 and node2:
merged_node.val = node1.val + node2.val
elif node1:
merged_node.val = node1.val
elif node2:
merged_node.val = node2.val
Comment on lines +52 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この場合、終端を表す番兵を使う手はありますよ。
TreeNode(0)
を node1, node2 にとりあえず入れておきます。

Copy link
Copy Markdown
Owner Author

@Fuminiton Fuminiton Mar 17, 2025

Choose a reason for hiding this comment

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

ご指摘ありがとうございます。
あまり番兵を使ったことがなかったので避けていましたが、ここかなり簡潔になりますね。

            node1 = node1 if node1 else TreeNode(0)
            node2 = node2 if node2 else TreeNode(0)
            merged_node.val = node1.val + node2.val

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

簡単になるのはここから下もです。

Copy link
Copy Markdown
Owner Author

@Fuminiton Fuminiton Mar 18, 2025

Choose a reason for hiding this comment

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

以下の三項演算子の部分も単にnode1.rightで済むようになるのですね。

                    node1.right if node1 else None,
                    node2.right if node2 else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if (node1 and node1.left) or (node2 and node2.left):

これもです。node1 node2 がある前提で書けます。

Copy link
Copy Markdown
Owner Author

@Fuminiton Fuminiton Mar 19, 2025

Choose a reason for hiding this comment

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

おっしゃる通りですね。
番兵使った方が明らかに簡潔になりました。

ご指摘ありがとうございます。

SENTINEL = TreeNode(0)

class Solution:
    def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
        if root1 is None and root2 is None:
            return None

        merged = TreeNode()
        stack = [(root1, root2, merged)]
        while stack:
            node1, node2, merged_node = stack.pop()
            if node1 is None:
                node1 = SENTINEL
            if node2 is None:
                node2 = SENTINEL
            merged_node.val = node1.val + node2.val            
            if node1.left is not None or node2.left is not None:
                merged_node.left = TreeNode()
                stack.append((node1.left, node2.left, merged_node.left))
            if node1.right is not None or node2.right is not None:
                merged_node.right = TreeNode()
                stack.append((node1.right, node2.right, merged_node.right))
        return merged


if (node1 and node1.left) or (node2 and node2.left):
merged_node.left = TreeNode()
stack.append((
node1.left if node1 else None,
node2.left if node2 else None,
merged_node.left
))
if (node1 and node1.right) or (node2 and node2.right):
merged_node.right = TreeNode()
stack.append((
node1.right if node1 else None,
node2.right if node2 else None,
merged_node.right
))

return merged
```

## step2
### 読んだコード
- https://github.com/rossy0213/leetcode/pull/12/files
- https://github.com/ryoooooory/LeetCode/pull/26/files
- https://github.com/fhiyo/leetcode/pull/25/files
- https://github.com/seal-azarashi/leetcode/pull/22/files
- https://github.com/Ryotaro25/leetcode_first60/pull/25/files

### 読んだ感想
再帰の方が自然に感じた。

一方で、簡単な実装だからというだけではなくて、合理的な理由も説明できるようにしていきたい。
観点は、可読性、探索対象の深度、メモリ使用量くらいか。
今回は、探索対象の深さが予測できてかつメモリの使用量も明示的に管理したいわけではないので、再帰で書いて読みやすさを重視したい。

https://github.com/seal-azarashi/leetcode/pull/22/files#r1778932434
> それで、どんなユースケースでこの mergeTrees は使われるんでしょうか。

root1を基準に更新していく解法が LeetCode 上の Solutions によく載っていたが、破壊的な実装なので微妙ではと思った。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

一部のノードを共有すると、他の破壊的なメソッドとの相性で残念なことになるかもしれませんね。


```python
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not root1 and not root2:
return None
if not root1 or not root2:
return root1 if root1 else root2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

三項演算子が読み手の認知負荷を上げてしまう可能はなくはないです。
olsen-blue/Arai60#5 (comment)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pythonの三項演算子読みにくいですね

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これ、私が感じるのは、なんで、わざわざ混ぜた後に分離するのか、です。

右手も左手も何も持っていない場合は、None を返してください。
右手に何も持っていない、または、左手に何も持っていない場合は、「右手に物を持っている場合は右手で持っているものを、そうでない場合は左手に持っているものを」返してください。

Python の三項演算子、目が左右に振られるんですよね。

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.

右手も左手も何も持っていない場合は、None を返してください。右手に何も持っていない、または、左手に何も持っていない場合は、「右手に物を持っている場合は右手で持っているものを、そうでない場合は左手に持っているものを」返してください。

確かに身近な言葉に直してみると、不自然な操作ですね
変な説明になっていないか考えながら実装しようと思います。

今回ですと、「右手に何も持っていない、または、左手に何も持っていない場合は、持っている方を返してください」(return root1 or root2)が自然かと思いました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

私は、
右手が空なら左手で持っているものを返してください。
左手が空なら右手で持っているものを返してください。
でいいと思います。


merged = TreeNode(root1.val + root2.val)
merged.left = self.mergeTrees(root1.left, root2.left)
merged.right = self.mergeTrees(root1.right, root2.right)
return merged
```

## step3
```python
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not root1 and not root2:
return None
if not root1:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Noneとの比較には is Noneを使うべきだと思います。
https://pep8-ja.readthedocs.io/ja/latest/#id41

また、 本当は if x is not None と書いているつもりで、 if x と書いている場合は注意してください - たとえば、デフォルトの値がNoneになる変数や引数に、何かしら別の値が設定されているかどうかをテストする場合です。この「別の値」は、ブール型のコンテクストでは 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.

レビュー、リンクの共有までありがとうございます。

return root2
if not root2:
return root1
merged = TreeNode(root1.val + root2.val)
merged.left = self.mergeTrees(root1.left, root2.left)
merged.right = self.mergeTrees(root1.right, root2.right)
return merged
```