diff --git a/.markdownlint.json b/.markdownlint.json index 8df6452..d3f5138 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -4,7 +4,7 @@ "MD002": true, "MD003": { "style": "atx" }, "MD004": { "style": "consistent" }, - "MD007": { "indent": 2 }, + "MD007": { "indent": 4 }, "MD009": { "br_spaces": 2 }, "MD012": true, "MD013": { @@ -20,6 +20,8 @@ "MD024": { "siblings_only": true }, "MD025": { "front_matter_title": "" }, "MD026": { "punctuation": ".,;:!。,;:" }, + "MD027": false, + "MD028": false, "MD029": false, "MD030": { "ul_single": 1, diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Python.md b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Python.md new file mode 100644 index 0000000..9a7ecb4 --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Python.md @@ -0,0 +1,367 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Python (CPython 3.11.10) +> 適用ルールセット: 共通5ルール + Python固有ルール +> 参照ファイル: references/common.md + references/python.md + +--- + +# Path Sum(パスの合計)— LeetCode 112|Python版 + +--- + +## 1. 問題分析 + +> 💡 **この問題は一言で言うと:** +> 「木の根(てっぺん)から葉(末端)まで下りていくルートの数値の合計が、目標値と一致するパスが1本でも存在するか?」を調べる問題です。 + +### CPython特有の注意点(最初に押さえるべき点) + +Pythonの再帰(関数が自分自身を呼び出すこと)にはデフォルトで **最大再帰深度(=関数が何段まで入れ子になれるか)** が `sys.getrecursionlimit()` で約1000に制限されています。今回の制約は最大5000ノードの木なので、**一直線に伸びた最悪形状の木では再帰深度が5000になりうる**点に注意が必要です。競技版では問題ありませんが、業務版では `sys.setrecursionlimit()` の設定や、再帰を使わない反復(イテレーション)での実装を検討する価値があります。 + +--- + +### 競技プログラミング視点 + +- 制約分析:ノード数 `[0, 5000]`。全ノードを一度ずつ訪れる O(n) で十分間に合う +- 最速手法:再帰DFS。ただしCPythonのデフォルト再帰制限(約1000)では、5000ノードの偏った木で RecursionError が発生する。競技環境や平均的なケースでは最速だが、最悪ケースの深さを考慮するなら sys.setrecursionlimit() を調整するか、反復DFS(スタック使用)が安全。 +- メモリ最小化:追加のデータ構造を使わず、関数呼び出しスタックのみ利用。空間計算量 O(h)(hは木の高さ) + +### 業務開発視点 + +- `Optional[TreeNode]`(= `TreeNode` か `None` のどちらかを表す型ヒント)で `None` の可能性を明示し、pylanceに型エラーを検出させる +- `root is None` チェックを先頭に置き、以降のコードで `root.val` に安全にアクセスできるようにする +- 業務版では反復DFSで再帰深度問題を回避し、プロダクション環境での安全性を高める + +### Python特有の分析 + +- `TreeNode | None`(Python 3.10以降の union 記法)または `Optional[TreeNode]` で null 安全性を表現 +- 今回は追加の標準ライブラリ(`collections`, `heapq` など)は不要。木の構造そのものを再帰で辿るシンプルな問題 +- 業務版の反復DFSでは `collections.deque`(=前後どちらからでも出し入れできる「両端開きの箱」)を活用し、先頭操作を O(1) にする + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速 +> - **再帰深度(Recursion Depth)**:関数が自分自身を呼び出す入れ子の深さ。Pythonはデフォルトで約1000まで +> - **DFS(深さ優先探索)**:木をできるだけ深く潜ってから引き返す探索方法。パス(根から葉への道)を追うのに適している +> - **O(h)**:木の高さ(height)に比例するメモリ使用量。均衡した木では O(log n)、一直線の木では O(n) + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 同じ問題でも複数の解き方があります。「速さ(時間計算量)」と「メモリ使用量(空間計算量)」、さらに「Pythonらしい書きやすさ」を比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ------------------- | ---------- | ---------- | ---------------- | ------ | ------------------- | -------------------- | ----------------------------- | +| ① 再帰DFS | O(n) | O(h) | 低 | ★★★ | なし | 適(シンプルで最速) | 競技版向け。再帰深度に注意 | +| ② 反復DFS(deque) | O(n) | O(h) | 中 | ★★☆ | `collections.deque` | 適(C実装deque) | 業務版向け。再帰深度問題なし | +| ③ BFS(幅優先探索) | O(n) | O(w) | 中 | ★☆☆ | `collections.deque` | 適 | wは最大幅。パス追跡には不向き | + +- **競技版に①再帰DFSを選ぶ理由**:コードが最も短く直感的。「木の各ノードに対して同じ処理を繰り返す」という再帰的性質をそのままコードで表現できる +- **業務版に②反復DFSを選ぶ理由**:最大5000ノードの一直線の木では再帰深度が5000に達し、Pythonのデフォルト再帰制限(約1000)を超える恐れがある。`deque` を使ったスタック管理で安全に回避できる + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **`collections.deque`**:前後どちらからでも O(1) で追加・取り出しできる「両端開きの箱」。`list` で先頭操作をすると O(n) かかるため、スタック・キューには `deque` が適している +> - **トレードオフ**:何かを得ると何かを失う関係。再帰は可読性が高いがスタック深度に限界があり、反復はその逆 + +--- + +## 3. 実装パターン + +> 💡 **コードの骨格(全体の構造)** +> +> 1. 入力検証(型チェック・制約確認) +> 2. 空の木(`root is None`)のエッジケース処理 +> 3. 各ノードで「残り目標値 = targetSum − 現在ノードの値」を計算しながら下に進む +> 4. 葉(子が両方 `None`)に到達したとき、残り目標値が 0 かどうかを確認して返す + +--- + +### 【業務開発版を使う場面】 + +チームで長期間メンテナンスするプロダクションコードに向きます。再帰深度の問題を `deque` で回避し、型ヒント・入力検証・docstringを充実させ、後から読んだ人がすぐ理解できる構造にしています。 + +```python +from typing import Optional +from collections import deque + + +# LeetCode 提供の TreeNode クラス(変更不可) +class TreeNode: + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + """ + Path Sum 解決クラス(業務開発版) + 再帰を使わず反復DFS(dequeスタック)で実装。 + Pythonの再帰深度制限を回避し、最大5000ノードの木でも安全に動作する。 + """ + + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + """ + 根から葉までのパスの合計が targetSum と等しいパスが存在するか判定する。 + + Args: + root: 二分木の根ノード(None の場合は空の木) + targetSum: 目標とするパスの合計値 + + Returns: + 条件を満たすパスが存在すれば True、存在しなければ False + + Raises: + TypeError: root が TreeNode でも None でもない場合 + TypeError: targetSum が int でない場合 + + Complexity: + Time: O(n) — 全ノードを最大1回ずつ訪問 + Space: O(h) — スタックの最大サイズは木の高さ h に比例 + """ + # --- 入力検証 --- + # targetSum の型チェック。Python は動的型付けなので、 + # 呼び出し元が float や str を渡してもコンパイル時には気づけない。 + # isinstance() で実行時に検証することで pylance + 実行時の両方で安全性を確保する。 + if not isinstance(targetSum, int): + raise TypeError( + f"targetSum must be int, got {type(targetSum).__name__}" + ) + + # root の型チェック。TreeNode または None のみを受け付ける。 + if root is not None and not isinstance(root, TreeNode): + raise TypeError( + f"root must be TreeNode or None, got {type(root).__name__}" + ) + + # --- エッジケース:空の木 --- + # root が None のとき、そもそもパスが存在しないので即 False を返す。 + if root is None: + return False + + # --- 反復DFS の準備 --- + # 再帰を避け、Python の再帰深度制限(約1000)を回避するために明示的なスタックを使用。 + # (ノード, 現時点までの残り目標値) のペアを積んでいく。 + # スタックとしての意図を明確にし、必要に応じて効率的な popleft() も可能な deque を採用。 + stack: deque[tuple[TreeNode, int]] = deque() + + # 最初は根ノードと初期の targetSum をスタックに積む。 + stack.append((root, targetSum)) + + # --- 反復DFS 本体 --- + # スタックが空になるまで繰り返す = 全パスを探索し終えるまで続ける。 + while stack: + # スタックの末尾(最後に積んだもの)を取り出す。 + # これが DFS(深さ優先)になる理由:直前に積んだ子を先に処理するから。 + node, remaining = stack.pop() + + # 残り目標値から現在のノードの値を引く。 + # 例)targetSum=22, node.val=5 → remaining=17(残りあと17必要) + remaining -= node.val + + # --- 葉(leaf)の判定 --- + # 葉 = 左の子も右の子も存在しないノード = パスの終点。 + is_leaf: bool = node.left is None and node.right is None + + if is_leaf: + # 葉に到達したとき、残り目標値がちょうど 0 になっていれば + # 「根からこの葉までの合計 = targetSum」が成立している。 + if remaining == 0: + return True + # 0 でなければこのパスは条件を満たさないので、次のパスへ。 + continue + + # --- 子ノードをスタックに積む --- + # 葉でない場合は、存在する子ノードを次の探索対象としてスタックに積む。 + # None チェックを行い、存在する子だけを積む(None を積んでしまうとエラーになる)。 + if node.right is not None: + # 右の子を先に積む。後で左の子を積むと、 + # スタックからは「左の子が先に取り出される」→ 左優先の DFS になる。 + stack.append((node.right, remaining)) + + if node.left is not None: + stack.append((node.left, remaining)) + + # すべてのパスを探索し終えたが条件を満たすものがなかった。 + return False +``` + +--- + +### 【競技プログラミング版を使う場面】 + +LeetCode などのオンラインジャッジで、制限時間内に正解を出すことが目的のコードに向きます。入力検証・docstring を省き、再帰DFS でコードを最短・最速にしています。ただし、最大ノード数が5000という制約のため、一直線に伸びた最悪ケースの木ではCPythonのデフォルト再帰制限(1000)を超えて `RecursionError` が発生するリスクがある点に注意してください。その場合は、反復処理(DFS/BFS)への書き換えや、`sys.setrecursionlimit()` による制限の引き上げが必要です。 + +```python +# Runtime 3 ms +# Beats 29.65% +# Memory 20.24 MB +# Beats 21.80% +from typing import Optional + + +class TreeNode: + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + # root が None = 空の木 or 葉を超えた = パスなし → False + # Python では "if not root:" でも動くが、 + # "root is None" の方が pylance が型を正確に絞り込める(型ガードとして機能する)。 + if root is None: + return False + + # 現在のノードの値を targetSum から引く。 + # こうすることで「残りいくら必要か」を次の階層に伝えられる。 + targetSum -= root.val + + # 葉(left も right も None)に到達したら、残りが 0 かどうかで答えを確定する。 + # Python の "and" は短絡評価(左が False なら右を評価しない)なので効率的。 + if root.left is None and root.right is None: + return targetSum == 0 + + # 葉でなければ、左右の子のどちらかで条件を満たすパスがあれば True。 + # "or" も短絡評価:左が True なら右は評価しない → 早期リターン。 + return ( + self.hasPathSum(root.left, targetSum) + or self.hasPathSum(root.right, targetSum) + ) +``` + +--- + +### 💡 動作トレース(Example 1) + +``` +入力: root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 + +木の形: + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ \ + 7 2 1 + +─── 競技版(再帰DFS)のトレース ─────────────────────────── + +Step 1: hasPathSum(node=5, targetSum=22) + → root is not None → 処理続行 + → targetSum = 22 - 5 = 17 + → 葉でない(left=4, right=8) + → 左 hasPathSum(4, 17) を先に評価(or の短絡評価) + +Step 2: hasPathSum(node=4, targetSum=17) + → targetSum = 17 - 4 = 13 + → 葉でない(left=11, right=None) + → 左 hasPathSum(11, 13) を評価 + +Step 3: hasPathSum(node=11, targetSum=13) + → targetSum = 13 - 11 = 2 + → 葉でない(left=7, right=2) + → 左 hasPathSum(7, 2) を評価 + +Step 4: hasPathSum(node=7, targetSum=2) + → targetSum = 2 - 7 = -5 + → 葉(left=None, right=None) + → -5 == 0 ? → False ❌ + +Step 5: hasPathSum(node=2, targetSum=2) ← or の右側を評価 + → targetSum = 2 - 2 = 0 + → 葉(left=None, right=None) + → 0 == 0 ? → True ✅ + +Step 6: True が or を通じて Step 3 → Step 2 → Step 1 へ伝播 +───────────────────────────────────────────────────── +出力: True ✅ +パス: 5 → 4 → 11 → 2 合計 = 22 +``` + +``` +─── 業務版(反復DFS + deque)のトレース ────────────────────── + +初期状態: + stack = [(node=5, remaining=22)] + +Iteration 1: pop → (node=5, remaining=22) + remaining = 22 - 5 = 17 + is_leaf = False(left=4, right=8) + → stack に right(8, 17) を追加 → stack = [(8, 17)] + → stack に left(4, 17) を追加 → stack = [(8, 17), (4, 17)] + +Iteration 2: pop → (node=4, remaining=17) ← 左が先に取り出される + remaining = 17 - 4 = 13 + is_leaf = False(left=11, right=None) + → right=None なのでスキップ + → stack に left(11, 13) を追加 → stack = [(8, 17), (11, 13)] + +Iteration 3: pop → (node=11, remaining=13) + remaining = 13 - 11 = 2 + is_leaf = False(left=7, right=2) + → stack に right(2, 2) を追加 → stack = [(8, 17), (2, 2)] + → stack に left(7, 2) を追加 → stack = [(8, 17), (2, 2), (7, 2)] + +Iteration 4: pop → (node=7, remaining=2) + remaining = 2 - 7 = -5 + is_leaf = True → -5 == 0 ? → False ❌ → continue + +Iteration 5: pop → (node=2, remaining=2) + remaining = 2 - 2 = 0 + is_leaf = True → 0 == 0 ? → True ✅ → return True +───────────────────────────────────────────────────── +出力: True ✅ +``` + +--- + +## 4. 検証 + +> 💡 エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。特に木の問題は「空の木」「葉が1枚だけ」「負の値が含まれる」などのケースでバグが起きやすいです。 + +| テストケース | 入力 | 期待出力 | 確認ポイント | +| ------------- | -------------------------------------------------------------- | -------- | ----------------------------- | +| 空の木 | `root=None, targetSum=0` | `False` | `root is None` の即時リターン | +| 葉1枚・一致 | `root=[1], targetSum=1` | `True` | 根が葉でもある場合 | +| 葉1枚・不一致 | `root=[1], targetSum=2` | `False` | 葉での等値判定 | +| 負の値を含む | `root=[-5,3], targetSum=-2` | `True` | `-5 + 3 = -2` 負数の合計 | +| 全部同じ値 | `root=[0,0,0], targetSum=0` | `True` | `0+0=0` のゼロ加算 | +| Example 1 | `root=[5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum=22` | `True` | 通常ケース | +| Example 2 | `root=[1,2,3], targetSum=5` | `False` | すべてのパスが不一致 | + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力・負の値など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **型ガード(Python)**:`if root is None: return False` のように書くと、以降のブロックで pylance が `root` を `TreeNode` 型として認識してくれる仕組み +> - **短絡評価(Short-circuit Evaluation)**:`A or B` で A が `True` なら B を評価しない、`A and B` で A が `False` なら B を評価しない仕組み。Pythonも同様に動作し、無駄な計算を防ぐ + +--- + +## Python版 vs TypeScript版 比較まとめ + +| 観点 | Python(競技版) | TypeScript(LeetCode版) | +| -------------- | --------------------------------------- | ---------------------------------------------- | +| null 安全 | `if root is None:` + pylance の型ガード | `if (root === null):` + TSコンパイラの型ガード | +| 再帰深度制限 | デフォルト約1000(業務版で回避が必要) | JavaScriptエンジン依存(通常より深い) | +| コード量 | 最短(動的型付け・`or` 一行で書ける) | やや長い(型注釈が必須) | +| 型安全性 | 型ヒント + pylance(オプショナル) | コンパイル時に強制(必須) | +| 業務版の優位点 | `deque` で再帰深度問題を回避 | `strict mode` でコンパイル時エラー検出 | diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Typescript.md b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Typescript.md new file mode 100644 index 0000000..6c56b20 --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Typescript.md @@ -0,0 +1,277 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: TypeScript +> 適用ルールセット: 共通5ルール + TS固有5ルール +> 参照ファイル: references/common.md + references/typescript.md + +--- + +# Path Sum(パスの合計)— LeetCode 112 + +--- + +## 1. 問題の分析 + +> 💡 **この問題は一言で言うと:** +> 「木の根(てっぺん)から葉(末端)まで下りる道の数値の合計が、目標値と一致するルートが存在するか?」を調べる問題です。 + +### 競技プログラミング視点での分析 + +木(ツリー)構造の問題では、**全ノード(節点)を一度ずつ訪れるだけで答えが出る**ため、最低でも O(n) の時間が必要です。各ノードで「今の合計が targetSum に達したか」をチェックしながら下に進めばよく、余計なメモリを使わずに解けます。 + +- 実行速度最優先 → **再帰DFS(深さ優先探索)**。関数呼び出しスタックのみを利用し、追加の配列不要 +- メモリ最小化 → 木の高さ分のスタックしか使わない。最悪 O(n)(一直線の木)、均衡木なら O(log n) + +### 業務開発視点での分析 + +- `TreeNode | null` という**Union型(複数の型を `|` でつなげた型)** を正確に扱う必要がある +- `null` チェックを忘れると実行時にクラッシュするため、型ガードで防御する +- 再帰関数はテストしやすく、ロジックが短く読みやすい → 保守性◎ + +### TypeScript特有の考慮点 + +- `root: TreeNode | null` を受け取る関数では、最初に `null` チェック(型ガード)を入れることで TypeScript コンパイラが以降の処理で `TreeNode` 型として扱えるようになる +- 今回は LeetCode 提供の `TreeNode` クラスを使うため、ジェネリクスは不要(型が固定されているため) +- `boolean` の戻り値型を明示することで、誤って数値や `undefined` を返すミスをコンパイル時に防げる + +> 📖 **このセクションで登場した用語** +> +> - **DFS(深さ優先探索)**:木を「できるだけ深く」潜ってから引き返す探索方法。迷路を一本道ずつ試すイメージ +> - **Union型**:`A | B` のように「AまたはB」を表す型。`TreeNode | null` は「ノードかnullのどちらか」を意味する +> - **型ガード**:`if (root === null)` のように実行時に型を絞り込む仕組み。以降のコードで型を安全に使えるようにする +> - **コンパイル時**:TypeScriptのコードをJavaScriptに変換する段階。ここでエラーを検出できると実行前にバグを防げる + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも複数の解き方があります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。「O(n)」は「ノードを全部一度ずつ見る」という意味です。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| --------------------------------- | ---------- | ---------- | ------------ | -------- | ------ | --------------------------------------- | +| ① 再帰DFS(残りの合計を減らす) | O(n) | O(h) | 低 | 高 | 高 | hは木の高さ。均衡木でO(log n)、最悪O(n) | +| ② 反復DFS(スタックを自前で管理) | O(n) | O(h) | 中 | 高 | 中 | スタックオーバーフローの心配なし | +| ③ BFS(幅優先探索、キューを使う) | O(n) | O(w) | 中 | 高 | 低 | wは木の最大幅。パスの復元には不向き | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(n)`:ノード数が2倍になると処理も約2倍(全ノードを一度見る) +> - `O(h)`:木の高さ分のメモリだけ使う(hはheightの略) +> - `O(log n)`:均衡した木では高さ ≈ log₂(n) なので、1000ノードでも高さは約10 + +> 📖 **このセクションで登場した用語** +> +> - **再帰(Recursion)**:関数が自分自身を呼び出す仕組み。「根→左の子→さらに左…」と自動的に深く潜れる +> - **スタック(Stack)**:後から積んだものを先に取り出す構造(皿の積み重ねイメージ)。再帰の内部実装でも使われている +> - **BFS(幅優先探索)**:木を「同じ深さ」の階層ごとに横向きに探索する方法 + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**: ① 再帰DFS(`targetSum` を減らしながら下に進む方法) + +- **理由**: + - BFS(方法③)を選ばない理由 → パスの「合計」を追跡するのに、幅広く横に探索するBFSより、根から葉まで縦に深く追うDFSの方が直感的で合う + - 反復DFS(方法②)を選ばない理由 → 制約がノード数5000以下のため、再帰の深さ上限に引っかかるリスクが極めて低い。実装もシンプルな再帰の方が読みやすい + - **再帰DFS(方法①)を選ぶ理由** → コードが最も短く、「左右それぞれの子を同じルールで調べる」という木の性質(再帰的構造)を自然に表現できる + +- **TypeScript特有の最適化ポイント**: + - `root === null` の null チェックで型ガードを使い、コンパイラに「この行以降は `TreeNode` 型が確定」と教える + - 戻り値 `boolean` を明示することで、誤って `void` や `undefined` を返すコードをコンパイル時にブロック + +> 📖 **このセクションで登場した用語** +> +> - **再帰的な構造**:木が「根+左の小さな木+右の小さな木」という同じ形の組み合わせでできていること。再帰関数と相性が良い +> - **null チェック(null guard)**:`null` の可能性がある値を使う前に `=== null` で確認する処理 + +--- + +## 4. 実装コード + +> 💡 **コード全体の骨格(構造の概要)** +> +> 1. `root` が `null` なら木が空(または葉を超えた)→ `false` を返す +> 2. 今いるノードが **葉(子が両方 null)** なら、残り合計が0かどうかを確認して答えを返す +> 3. 葉でなければ、左の子・右の子のどちらかで条件を満たすパスがあるか再帰的に確認する + +```typescript +// Runtime 0 ms +// Beats 100.00% +// Memory 59.53 MB +// Beats 41.61% +/** + * 二分木の根から葉までのパスの合計が targetSum と等しいパスが存在するか判定する + * @param root - 二分木の根ノード(null の場合は空の木) + * @param targetSum - 目標とする合計値 + * @returns パスが存在すれば true、存在しなければ false + * @complexity Time: O(n), Space: O(h) n=ノード数, h=木の高さ + */ +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + // --- ベースケース①:root が null の場合 --- + // 木が空、または葉を超えて「存在しないノード」に来た場合。 + // このルートにはパスが存在しないので false を返す。 + if (root === null) { + return false; + // ↑ TypeScriptの型ガード:この行以降 root は TreeNode 型として確定する + } + + // --- ベースケース②:現在のノードが「葉(leaf)」かどうかを確認 --- + // 葉とは「左の子も右の子も存在しないノード」のこと。 + // 根から葉まで来たということは、パスの終点に到達したことを意味する。 + const isLeaf: boolean = root.left === null && root.right === null; + + if (isLeaf) { + // 葉に到達したとき、残りの目標値(targetSum)がちょうど現在のノードの値と等しければ + // 「このパスの合計 = 最初の targetSum」が成立する。 + // *ここで root.val を引いた結果が 0 になるか、直接比較するかは好みだが + // 「今のノードの値で最後の差し引きが済む」と考えると分かりやすい。 + return root.val === targetSum; + } + + // --- 再帰ステップ:左の子・右の子へ潜る --- + // 現在のノードの値 (root.val) を targetSum から引くことで + // 「残りあとどれだけ合計が必要か」を次の階層に渡す。 + // 例:targetSum=22, root.val=5 → 次の階層では targetSum=17 を目指す + const remaining: number = targetSum - root.val; + + // 左の子ツリーか、右の子ツリーのどちらか一方でも条件を満たすパスがあれば true。 + // || (論理和)なので、どちらかが true なら即座に true を返す(短絡評価)。 + return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining); +} +``` + +--- + +### 💡 動作トレース(Example 1 での変数変化) + +``` +入力: root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 + +木の形(図解): + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ \ + 7 2 1 + +───────────────────────────────────────────────────── +Step 1: hasPathSum(5, 22) + → root=5, isLeaf=false + → remaining = 22 - 5 = 17 + → 左(4, 17) と 右(8, 17) を調べる + +Step 2: hasPathSum(4, 17) ← 左の子を先に調べる + → root=4, isLeaf=false + → remaining = 17 - 4 = 13 + → 左(11, 13) と 右(null, 13) を調べる + +Step 3: hasPathSum(11, 13) + → root=11, isLeaf=false + → remaining = 13 - 11 = 2 + → 左(7, 2) と 右(2, 2) を調べる + +Step 4: hasPathSum(7, 2) + → root=7, isLeaf=true(子が両方null) + → 7 === 2 ? → false ❌ + +Step 5: hasPathSum(2, 2) + → root=2, isLeaf=true(子が両方null) + → 2 === 2 ? → true ✅ ← ここで条件達成! + +Step 6: Step 5 の true が || で Step 3 に伝わる → true +Step 7: Step 3 の true が || で Step 2 に伝わる → true +Step 8: Step 2 の true が || で Step 1 に伝わる → true +───────────────────────────────────────────────────── +出力: true +パス: 5 → 4 → 11 → 2 合計 = 22 ✅ +``` + +``` +入力: root = [1,2,3], targetSum = 5 + +木の形: + 1 + / \ + 2 3 + +Step 1: hasPathSum(1, 5) + → remaining = 5 - 1 = 4 + → 左(2, 4) と 右(3, 4) を調べる + +Step 2: hasPathSum(2, 4) + → root=2, isLeaf=true → 2 === 4 ? → false ❌ + +Step 3: hasPathSum(3, 4) + → root=3, isLeaf=true → 3 === 4 ? → false ❌ + +Step 4: false || false → false +───────────────────────────────────────────────────── +出力: false ✅ +``` + +``` +入力: root = [], targetSum = 0 (空の木) + +Step 1: hasPathSum(null, 0) + → root === null → 即 false を返す +───────────────────────────────────────────────────── +出力: false ✅ +``` + +> 📖 **このセクションで登場した用語** +> +> - **ベースケース(Base Case)**:再帰関数が「これ以上潜らなくていい」と判断して結果を返す条件。ここでは「null に到達」と「葉に到達」の2つ +> - **葉(Leaf Node)**:子ノードが1つも存在しない末端のノード。パスの終点となる +> - **短絡評価(Short-circuit Evaluation)**:`A || B` で A が true なら B を評価せず即 true を返す仕組み。無駄な処理を省ける +> - **再帰ステップ(Recursive Step)**:関数が自分自身を呼び出す部分。今回は左右の子ノードに対して同じ処理を繰り返す +> - **remaining**:「残り目標合計」。現在のノードの値を差し引いた後、次の階層に渡す値 + +--- + +## TypeScript固有の最適化観点まとめ + +### 型安全性の活用 + +```typescript +// ❌ JavaScriptだと null チェックを忘れても実行時まで気づけない +function hasPathSumJS(root, targetSum) { + return root.val === targetSum; // null.val → クラッシュ! +} + +// ✅ TypeScriptなら root: TreeNode | null と宣言するだけで +// コンパイラが「null かもしれないのに使っている」と警告してくれる +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + if (root === null) return false; // ← 型ガードでここ以降は TreeNode 確定 + // この行では root.val に安全にアクセスできる + return root.val === targetSum; +} +``` + +**JavaScriptにはない理由**: JavaScriptは実行するまで型のミスに気づけません。TypeScriptの `TreeNode | null` という型宣言により、コンパイル時(コードをJavaScriptに変換する段階)に「null の可能性があるのに使っている」とエラーを出してくれます。 + +### readonly 修飾子について + +今回は LeetCode が `TreeNode` クラスを提供するため自分で定義しませんが、業務コードとして自分で定義するなら以下のように `readonly` をつけることで意図しない書き換えを防げます: + +```typescript +// 業務開発版:ノードの値が外から書き換えられないように readonly で守る +class SafeTreeNode { + readonly val: number; // val を書き換え不可にする(JavaScriptにはない保護) + readonly left: SafeTreeNode | null; + readonly right: SafeTreeNode | null; + + constructor(val = 0, left: SafeTreeNode | null = null, right: SafeTreeNode | null = null) { + this.val = val; + this.left = left; + this.right = right; + } +} +``` + +> 📖 **このセクションで登場した用語** +> +> - **readonly**:変数の値を変更できないようにする修飾子。JavaScriptにはなく、TypeScript独自の機能。意図せぬ書き換えをコンパイル時(実行前)に防げる +> - **型ガード(Type Guard)**:`if (root === null)` のように実行時に型を絞り込む処理。TypeScriptコンパイラはこれを認識し、以降のブロックで型を自動的に絞り込んでくれる(型推論の一種) +> - **コンパイル時エラー**:TypeScriptをJavaScriptに変換する段階で検出するエラー。実行前に気づけるため、実行時クラッシュより安全 diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README.md b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README.md new file mode 100644 index 0000000..07761fc --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README.md @@ -0,0 +1,713 @@ +# Path Sum — 根から葉へのパス合計判定 + +

目次

+ +- [概要](#overview) +- [アルゴリズム要点 TL;DR](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython 最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +💡 **この問題は一言で言うと:** +「木の頂点(根)から末端(葉)まで下りていくルートの数値の合計が、指定された目標値と一致するパスが1本でも存在するか?」を調べる問題です。 + +## なぜ難しいのか・どこにポイントがあるのか + +この問題の難しさは「全パスを効率よく探索する」点にあります。木(ツリー)は配列やリストと違い、「インデックス」で直接ノードにアクセスできません。根から葉まで分岐しながら下りていく必要があり、**どの経路を辿ったか**を追跡しながら合計を管理する仕組みが求められます。また、葉の定義(左右両方の子が存在しないノード)を正確に判定しないと、途中のノードで誤って答えを確定してしまうバグが発生します。 + +### 問題の制約 + +| 項目 | 制約 | +| ---------- | -------------------- | +| ノード数 | 0 以上 5000 以下 | +| ノードの値 | -1000 以上 1000 以下 | +| targetSum | -1000 以上 1000 以下 | + +> 📖 **この章で登場した用語** +> +> - **根(Root)**:木の一番上にあるノード。ここから探索を始める +> - **葉(Leaf)**:子ノードが1つも存在しないノード。パスの終点となる +> - **パス(Path)**:根から葉まで辿った一本の道。途中で分岐に戻ることはない +> - **制約**:入力として与えられる値の範囲や条件のこと + +--- + +

アルゴリズム要点 TL;DR

+ +💡 **TL;DR(Too Long; Didn't Read)**とは「長くて読めない人向けの要約」という意味です。アルゴリズム全体の戦略をここで掴んでおき、詳細は後の章で確認しましょう。 + +- **戦略**:DFS(深さ優先探索)で根から葉まで下りながら、`targetSum` から現在のノードの値を引いていく。葉に到達した時点で残り値が 0 なら条件達成。 + - _なぜ DFS か?_ → 「根から葉への1本道を完走する」という目的に、深く潜ってから戻る DFS の性質がぴったり合うから +- **データ構造**:競技版は再帰呼び出しスタック(関数が自分自身を呼ぶ仕組み)、業務版は `collections.deque`(両端開きの箱型構造)を明示的なスタックとして使用 +- **時間計算量**:O(n)。すべてのノードを最大1回ずつ訪問するため +- **空間計算量**:O(h)。h は木の高さ。均衡した木では O(log n)、一直線の木では O(n) +- **メモ化**:今回は不要。同じノードを複数回訪れる経路が存在しないため + +> 📖 **この章で登場した用語** +> +> - **DFS(深さ優先探索 / Depth-First Search)**:木やグラフをできるだけ深く潜ってから引き返す探索方法。迷路を一本道ずつ試すイメージ +> - **`collections.deque`**:前後どちらからでも O(1) で追加・取り出しができる「両端開きの箱」。`list` で先頭操作をすると O(n) かかるのに対して高速 +> - **スタック(Stack)**:後から積んだものを先に取り出す構造(皿の積み重ねイメージ) +> - **メモ化**:一度計算した結果を記録しておき、同じ計算を繰り返さない技法 + +--- + +

図解

+ +💡 **Mermaid フローチャートの読み方**:ひし形(`{}`)は「Yes/No で分岐する条件判定」を表し、長方形(`[]`)は「実際に行う処理のステップ」を表します。矢印(`-->`)はデータや制御の流れを示します。図は上から下へ読み進めてください。 + +--- + +### フローチャート + +この図は `hasPathSum` 関数の処理の流れ全体を表しています。「根に入力 → null チェック → 葉チェック → 再帰」という順番で処理が進む様子を上から下へ読んでください。 + +```mermaid +flowchart TD + Start[Start hasPathSum root targetSum] + Start --> NullCheck{root is None} + NullCheck -- Yes --> RetFalse[Return False] + NullCheck -- No --> Subtract[remaining = targetSum - root.val] + Subtract --> LeafCheck{left is None AND right is None} + LeafCheck -- Yes --> ZeroCheck{remaining == 0} + ZeroCheck -- Yes --> RetTrue[Return True] + ZeroCheck -- No --> RetFalse2[Return False] + LeafCheck -- No --> RecurLeft[hasPathSum left remaining] + RecurLeft --> OrCheck{left returned True} + OrCheck -- Yes --> RetTrue2[Return True] + OrCheck -- No --> RecurRight[hasPathSum right remaining] + RecurRight --> RetResult[Return right result] +``` + +**各ノードの意味:** + +- `Start`:関数の入り口。`root`(現在のノード)と `targetSum`(残り目標値)を受け取る +- `NullCheck`(ひし形):`root` が `None` かどうかを判定。空の木または葉を超えた場合に該当 +- `Subtract`:現在のノードの値を `targetSum` から引いて「残り目標値」を計算する +- `LeafCheck`(ひし形):左右両方の子が `None` = 葉に到達したかを判定する +- `ZeroCheck`(ひし形):葉に到達した時点で残り目標値がちょうど 0 かどうかを確認する +- `RecurLeft`:左の子ツリーに対して同じ処理を再帰的に呼び出す +- `OrCheck`(ひし形):左の子で条件達成(`True`)が返ったか。`True` なら右の子を調べる必要がない(短絡評価) + +--- + +### データフロー図(木の探索の様子) + +この図は Example 1 の木を、DFS がどの順番でノードを訪問するかを表しています。左から右へ、探索が進む順番に読んでください。 + +```mermaid +graph LR + subgraph Tree + N5[val=5] + N4[val=4] + N8[val=8] + N11[val=11] + N13[val=13] + N4b[val=4] + N7[val=7 leaf] + N2[val=2 leaf GOAL] + N1[val=1 leaf] + end + subgraph DFS_order + D1[Visit 5 rem=22] + D2[Visit 4 rem=17] + D3[Visit 11 rem=13] + D4[Visit 7 rem=2 False] + D5[Visit 2 rem=0 True] + end + N5 --> N4 + N5 --> N8 + N4 --> N11 + N8 --> N13 + N8 --> N4b + N11 --> N7 + N11 --> N2 + N4b --> N1 + D1 --> D2 + D2 --> D3 + D3 --> D4 + D3 --> D5 +``` + +**主要な流れの説明:** + +- `N5 → N4 → N11`:DFS が左の子を優先して深く潜っていく経路 +- `Visit 7 rem=2 False`:葉ノード 7 で残り 2 ≠ 0 のため不一致 +- `Visit 2 rem=0 True`:葉ノード 2 で残り 0 = 条件達成!ここで `True` が返される + +--- + +> 💡 **代表例でのトレース**:`root=[5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum=22` を入力として、フローチャートの各ノードをどのように通過するかを示します。 +> +> ``` +> 初期状態: root=Node(5), targetSum=22 +> +> Step 1: NullCheck → root=Node(5) ≠ None → No +> Subtract → remaining = 22 - 5 = 17 +> LeafCheck → left=Node(4), right=Node(8) → No(葉ではない) +> → 左の子 Node(4) に進む +> +> Step 2: NullCheck → root=Node(4) ≠ None → No +> Subtract → remaining = 17 - 4 = 13 +> LeafCheck → left=Node(11), right=None → No(葉ではない) +> → 左の子 Node(11) に進む +> +> Step 3: NullCheck → root=Node(11) ≠ None → No +> Subtract → remaining = 13 - 11 = 2 +> LeafCheck → left=Node(7), right=Node(2) → No(葉ではない) +> → 左の子 Node(7) に進む +> +> Step 4: NullCheck → root=Node(7) ≠ None → No +> Subtract → remaining = 2 - 7 = -5 +> LeafCheck → left=None, right=None → Yes(葉!) +> ZeroCheck → -5 == 0 ? → No → return False ❌ +> +> Step 5: Step 3 に戻り、右の子 Node(2) を調べる +> NullCheck → root=Node(2) ≠ None → No +> Subtract → remaining = 2 - 2 = 0 +> LeafCheck → left=None, right=None → Yes(葉!) +> ZeroCheck → 0 == 0 ? → Yes → return True ✅ +> +> 最終結果: True(パス 5→4→11→2 の合計 = 22) +> ``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理 +> - **短絡評価(Short-circuit Evaluation)**:`A or B` で A が `True` なら B を評価しない仕組み。無駄な計算を省く +> - **DFS 探索順序**:左の子 → 右の子 の順で深く潜る(左優先DFS) + +--- + +

正しさのスケッチ

+ +💡 この章では「このアルゴリズムが常に正しい答えを返すと言える理由」を整理します。数学的な厳密な証明ではなく、「なぜ正しいと言えるか」の直感的な説明です。 + +### ① 不変条件(ループ・再帰を通じてずっと成り立つ条件) + +> **「`hasPathSum(node, remaining)` を呼び出したとき、`remaining` は根から `node` の親までの合計を `targetSum` から引いた残り値である」** + +- 根ノードでの呼び出し `hasPathSum(root, 22)` では `remaining=22`(まだ何も引いていない) +- 次の階層 `hasPathSum(node.left, 22-5=17)` では `remaining=17`(根ノード 5 を引いた残り) +- この不変条件が成り立ち続けることで、葉に到達したときに `remaining==0` が「合計が一致した」を意味することが保証される + +### ② 網羅性(すべてのパスを見落とさない) + +- 各ノードで左の子と右の子の**両方**を調べる(`or` の左右) +- `or` の短絡評価により、左で `True` が得られた場合は右を調べないが、`False` の場合は必ず右も調べる +- このため、木のすべての葉へのパスが必ず探索される + +### ③ 基底条件(再帰が終わる条件) + +- **ケース A**:`root is None` → これ以上下に進めない(子が存在しない)→ `False` を返す。空の木や、葉の子(存在しない)を参照した場合に該当 +- **ケース B**:`root.left is None and root.right is None`(葉)→ ここでパスが完了した → `remaining == 0` の真偽値を返す + +### ④ 終了性(アルゴリズムが必ず終わる理由) + +- 各再帰呼び出しでは `node.left` または `node.right` に進む +- 木は有限(最大 5000 ノード)であり、葉から先は必ず `None`(ケース A の基底条件)になる +- したがって再帰の深さは最大で木の高さ h に抑えられ、必ず終了する + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 +> - **基底条件**:再帰の終了条件。これがないと無限ループになる +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 + +--- + +

計算量

+ +💡 計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| ---------- | ---------------------- | -------------------------- | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(log n)` | 入力の対数に比例 | 二分探索で半分ずつ絞る | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +### 時間計算量:O(n) + +すべてのノードをちょうど1回ずつ訪問します。n = 5000 のとき最大 5000 回の関数呼び出しで答えが出ます。「最良ケース(根が葉の場合)」でも「最悪ケース(すべてのノードを見る場合)」でも O(n) であることに変わりありません。 + +### 空間計算量:O(h)(hは木の高さ) + +再帰呼び出しのたびに関数の情報がコールスタック(関数呼び出しの履歴を積み重ねる領域)に積まれます。その最大の深さは木の高さ h です。 + +| 木の形状 | 高さ h | 空間計算量 | +| ------------------------ | -------- | --------------------------- | +| 均衡二分木(左右均等) | O(log n) | O(log n) ≈ O(13) for n=5000 | +| 一直線(右の子のみ続く) | O(n) | O(n) = O(5000) | + +### 競技版 vs 業務版の空間計算量の違い + +| 実装 | スタック管理 | 空間計算量 | 特記 | +| ------------------------ | -------------------------------- | ---------- | ------------------------------ | +| 再帰DFS(競技版) | Pythonの呼び出しスタック(暗黙) | O(h) | デフォルト再帰制限約1000に注意 | +| 反復DFS+deque(業務版) | `deque` として明示的に管理 | O(h) | 再帰制限なし。安全 | + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **コールスタック**:関数が呼び出されるたびにその情報を積み上げる領域。再帰の深さに比例して使われる +> - **均衡二分木**:左右の子の高さがほぼ等しい木。高さが log n に抑えられるため効率的 + +--- + +

Python 実装

+ +💡 コードを読む前に、実装の全体的な骨格を確認しましょう。 + +**競技版(再帰DFS)の骨格:** + +1. `root is None` チェック → `False` を返す(空の木または存在しない子を参照) +2. `targetSum` から現在のノードの値を引く +3. 葉かどうかを判定 → 葉なら `remaining == 0` を返す +4. 葉でなければ左右の子に対して再帰呼び出し。どちらかが `True` なら `True` + +**業務版(反復DFS + deque)の骨格:** + +1. 入力検証(型チェック) +2. 空の木チェック → `False` を返す +3. `deque` にルートを積み、ループ開始 +4. ループ内:取り出す → 残り値を計算 → 葉なら確定 → 子をスタックに積む +5. ループ終了まで条件達成がなければ `False` + +--- + +### 競技プログラミング版 + +チームメンバーのいない個人開発や、LeetCode などのジャッジで制限時間内に正解を出すことを優先する場面に向きます。型ヒントは最低限、エラーハンドリングを省略し、コードの短さと速度を最大化しています。 + +```python +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + pass + +# LeetCode が提供する TreeNode の定義(コメントアウトされているため、 +# 実行時は NameError にならないよう try/except で軽量フォールバックを用意する) +try: + # LeetCode 環境では TreeNode がすでに定義済みなので、この定義は上書きされる + TreeNode # type: ignore[used-before-def] +except NameError: + class TreeNode: # type: ignore[no-redef] + """二分木の1つのノードを表すクラス(最小定義)""" + __slots__ = ("val", "left", "right") + + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + # ベースケース①:root が None = 空の木 or 葉を超えた + # None のノードにはパスが存在しないので即 False を返す。 + # "root is None" は "root == None" より高速(同一性チェック)かつ pylance 推奨。 + if root is None: + return False + + # 現在のノードの値を targetSum から引く。 + # 「残りあとどれだけ合計が必要か」を次の階層に渡すため引き算で管理する。 + # 例)targetSum=22, root.val=5 → 次の呼び出しには 17 を渡す + targetSum -= root.val + + # ベースケース②:葉(左右両方の子が None)に到達した + # パスの終点に来たので、残り目標値がちょうど 0 かどうかで答えを確定する。 + if root.left is None and root.right is None: + return targetSum == 0 + + # 再帰ステップ:左の子 or 右の子のどちらかで条件を満たすパスがあれば True。 + # Python の "or" は短絡評価(左が True なら右を評価しない)なので、 + # 左で True が返った時点で右の探索をスキップできる。 + return ( + self.hasPathSum(root.left, targetSum) + or self.hasPathSum(root.right, targetSum) + ) +``` + +--- + +### 業務開発版(反復DFS + deque) + +チームで長期間メンテナンスするプロダクションコードに向きます。Python のデフォルト再帰制限(約1000)を回避するため `deque` を使った明示的なスタック管理を採用し、最大5000ノードの木でも安全に動作します。 + +```python +from __future__ import annotations + +from collections import deque +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + pass + +try: + TreeNode # type: ignore[used-before-def] +except NameError: + class TreeNode: # type: ignore[no-redef] + """二分木の1つのノードを表すクラス(最小定義)""" + __slots__ = ("val", "left", "right") + + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + """ + 根から葉までのパスの合計が targetSum と等しいパスが存在するか判定する。 + + 反復DFS(deque スタック)で実装。 + Python のデフォルト再帰制限(約1000)を回避し、 + 最大 5000 ノードの一直線の木でも安全に動作する。 + + Args: + root: 二分木の根ノード(None の場合は空の木) + targetSum: 目標とするパスの合計値 + + Returns: + 条件を満たすパスが存在すれば True、存在しなければ False + + Raises: + TypeError: targetSum が int でない場合 + TypeError: root が TreeNode でも None でもない場合 + + Complexity: + Time: O(n) — 全ノードを最大1回ずつ訪問 + Space: O(h) — deque の最大サイズは木の高さ h に比例 + """ + # --- 入力検証 --- + # Python は動的型付けなので呼び出し元が誤った型を渡してもコンパイル時には気づけない。 + # isinstance() で実行時に検証し、pylance の型チェックと合わせて二重に安全性を確保する。 + if not isinstance(targetSum, int): + raise TypeError( + f"targetSum must be int, got {type(targetSum).__name__}" + ) + + # root は TreeNode または None のみ許可する + if root is not None and not isinstance(root, TreeNode): + raise TypeError( + f"root must be TreeNode or None, got {type(root).__name__}" + ) + + # --- エッジケース:空の木 --- + # root が None のとき、そもそもパスが存在しないので即 False を返す。 + if root is None: + return False + + # --- 反復DFS の準備 --- + # deque をスタック(後から積んだものを先に取り出す)として使う。 + # タプル (ノード, 残り目標値) の形で積んでいく。 + # list でも動くが、deque.pop() は C 実装で若干高速。 + # また list.append() / list.pop() は O(1) だが、 + # deque の方が「スタックとして使う」という意図が明確で可読性も高い。 + stack: deque[tuple[TreeNode, int]] = deque() + stack.append((root, targetSum)) + + # --- 反復DFS 本体 --- + # スタックが空になる = 探索できるパスをすべて試し終えた + while stack: + # スタックの末尾(最後に積んだもの)を取り出す。 + # これが DFS(深さ優先)になる理由: + # 直前に積んだ子ノードを先に処理するため、自然と深く潜る探索になる。 + node, remaining = stack.pop() + + # 現在のノードの値を残り目標値から引く。 + # これにより「このノードを通過した分の合計」を差し引いて次に渡せる。 + remaining -= node.val + + # --- 葉(Leaf)の判定 --- + # 葉 = 左の子も右の子も存在しないノード = パスの終点 + is_leaf: bool = node.left is None and node.right is None + + if is_leaf: + # 葉に到達したとき、残り目標値がちょうど 0 になっていれば + # 「根からこの葉までの合計 = targetSum」が成立している。 + if remaining == 0: + return True + # 0 でなければこのパスは条件を満たさないので次の探索へ + continue + + # --- 子ノードをスタックに積む --- + # 右の子を先に積む(後でスタックから取り出すとき左が先になる = 左優先DFS)。 + # None の子はスタックに積まない(積むと TypeError が発生する)。 + if node.right is not None: + stack.append((node.right, remaining)) + if node.left is not None: + stack.append((node.left, remaining)) + + # すべてのパスを探索し終えたが、条件を満たすものがなかった + return False +``` + +--- + +> 💡 **動作トレース(業務版 · deque)**:`root=[5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum=22` +> +> ``` +> 初期状態: stack = [(Node(5), 22)] +> +> Iteration 1: pop → (Node(5), 22) +> remaining = 22 - 5 = 17 +> is_leaf = False(left=Node(4), right=Node(8)) +> → right: append (Node(8), 17) → stack = [(Node(8), 17)] +> → left: append (Node(4), 17) → stack = [(Node(8), 17), (Node(4), 17)] +> +> Iteration 2: pop → (Node(4), 17) ← 左が先に取り出される(左優先DFS) +> remaining = 17 - 4 = 13 +> is_leaf = False(left=Node(11), right=None) +> → right: None なのでスキップ +> → left: append (Node(11), 13) → stack = [(Node(8), 17), (Node(11), 13)] +> +> Iteration 3: pop → (Node(11), 13) +> remaining = 13 - 11 = 2 +> is_leaf = False(left=Node(7), right=Node(2)) +> → right: append (Node(2), 2) → stack = [(Node(8), 17), (Node(2), 2)] +> → left: append (Node(7), 2) → stack = [(Node(8), 17), (Node(2), 2), (Node(7), 2)] +> +> Iteration 4: pop → (Node(7), 2) +> remaining = 2 - 7 = -5 +> is_leaf = True(left=None, right=None) +> -5 == 0 ? → False ❌ → continue +> +> Iteration 5: pop → (Node(2), 2) +> remaining = 2 - 2 = 0 +> is_leaf = True(left=None, right=None) +> 0 == 0 ? → True ✅ → return True +> +> 最終結果: True(パス 5→4→11→2 の合計 = 22) +> ``` + +> 📖 **この章で登場した用語** +> +> - **`from __future__ import annotations`**:型ヒントを文字列として扱うようにする宣言。前方参照(まだ定義されていないクラスを型として使う)を解決できる +> - **`TYPE_CHECKING`**:`True` になるのは pylance などの静的型チェック時のみ。実行時は `False` なので、型チェック用のインポートを実行時に省略できる +> - **`__slots__`**:クラスが持てる属性を事前に宣言する仕組み。通常のクラスより使うメモリを削減できる +> - **`Optional[TreeNode]`**:`TreeNode` または `None` のどちらかであることを表す型ヒント。`TreeNode | None` とも書ける(Python 3.10以降) +> - **`deque[tuple[TreeNode, int]]`**:「TreeNode と int のペア(タプル)を格納する deque」を表す型ヒント + +--- + +

CPython 最適化ポイント

+ +💡 この章では「同じ処理でも Python の書き方によって速さが変わる理由」を説明します。最適化テクニックは「最適化前 → 最適化後 → なぜ速くなるか」の3点セットで確認しましょう。 + +### ① `root is None` vs `root == None` + +```python +# 最適化前(pylance 警告が出ることもある) +if root == None: + return False + +# 最適化後(推奨) +if root is None: + return False +# 理由:`is` は同一性チェック(メモリアドレスが同じか)なので +# `==` のような値の比較(__eq__ メソッドの呼び出し)が発生しない。 +# None との比較では is を使うのが Python の慣習であり、pylance も推奨する。 +``` + +### ② 葉判定の変数化 + +```python +# 最適化前:同じ属性アクセスが2回発生する +if node.left is None and node.right is None: + if node.left is None and node.right is None: # 仮に再チェックが必要な場合 + ... + +# 最適化後:bool 変数に1回だけ評価してキャッシュする +is_leaf: bool = node.left is None and node.right is None +if is_leaf: + ... +# 理由:属性アクセス(node.left, node.right)は内部的にオブジェクトの辞書を +# 検索するコストがかかる。結果を bool 変数に保存すると1回の検索で済む。 +# 今回は微小な差だが、ループ回数が多い場面では効果が出る。 +``` + +### ③ `or` の短絡評価による早期リターン + +```python +# 競技版での実装 +return ( + self.hasPathSum(root.left, targetSum) # ← まずこちらを評価 + or self.hasPathSum(root.right, targetSum) # True なら右は評価しない +) +# 理由:Python の `or` は左が True の瞬間に右を評価せず即 True を返す(短絡評価)。 +# 左のサブツリーで答えが見つかった場合、右のサブツリー全体の探索をスキップできる。 +# 最良ケースでは探索を半分に抑えられる。 +``` + +### ④ `deque` と `list` の違い(業務版で deque を選ぶ理由) + +```python +# list をスタックとして使う場合(今回は末尾操作のみなので実は同等) +stack_list: list[tuple[TreeNode, int]] = [] +stack_list.append((node, remaining)) # O(1) 均償(amortized) +stack_list.pop() # O(1) + +# deque をスタックとして使う場合 +from collections import deque +stack_deque: deque[tuple[TreeNode, int]] = deque() +stack_deque.append((node, remaining)) # O(1) 常に +stack_deque.pop() # O(1) 常に + +# 理由:末尾操作だけなら list でも deque でも速度差はほぼない。 +# ただし deque を使うことで「このデータ構造はスタック/キューとして使う」 +# という意図が明確になり、可読性と保守性が向上する。 +# 先頭への追加・削除が必要になったとき、list の O(n) と deque の O(1) の差が大きい。 +``` + +> 📖 **この章で登場した用語** +> +> - **同一性チェック(`is`)**:2つの変数が全く同じオブジェクトを指しているかを確認する操作。`None` のチェックに適している +> - **属性アクセス**:`node.left` のようにオブジェクトのプロパティを参照する操作。Pythonでは内部的に辞書検索が発生する +> - **短絡評価(Short-circuit Evaluation)**:`A or B` で A が True なら B を評価しない仕組み。早期リターンによる最適化 +> - **均償 O(1)(Amortized O(1))**:「1回ごとに見ると遅い操作があるが、平均すると O(1) になる」こと。`list.append()` はバッファ再確保が稀に O(n) になるが平均は O(1) + +--- + +

エッジケースと検証観点

+ +💡 エッジケースとは「入力が空・最小値・最大値・特殊な形状」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。各ケースでなぜ問題になりうるかを先に確認しておきましょう。 + +| # | テストケース | 入力 | 期待出力 | なぜ注意が必要か | +| --- | ------------------------------ | ---------------------------------- | -------- | ----------------------------------------------------------------------- | +| 1 | **空の木** | `root=None, targetSum=0` | `False` | `root is None` チェックがないと `root.val` で AttributeError が発生する | +| 2 | **葉が1枚・一致** | `root=[1], targetSum=1` | `True` | 根が同時に葉でもあるケース。葉の判定ロジックが正しいか確認 | +| 3 | **葉が1枚・不一致** | `root=[1], targetSum=2` | `False` | 葉での `remaining == 0` 判定が正確か確認 | +| 4 | **負の値を含む** | `root=[-3,1], targetSum=-2` | `True` | `-3 + 1 = -2`。負数の引き算が正しく動くか確認 | +| 5 | **targetSum がゼロ** | `root=[0,0,0], targetSum=0` | `True` | `0 + 0 = 0`。ゼロの加算が正しく判定されるか確認 | +| 6 | **合計がゼロになるが葉でない** | `root=[0,0], targetSum=0` | `True` | 根(`val=0`)を通過して、子(`val=0`)の葉で確定 | +| 7 | **一直線の右の木(最悪深さ)** | `root=[1→2→...→5000], targetSum=X` | 各値 | 競技版は再帰深度 5000 に達する可能性。業務版の deque は安全 | +| 8 | **値の境界最大** | `root=[1000], targetSum=1000` | `True` | 制約の上限値 1000 で正しく動くか | +| 9 | **値の境界最小** | `root=[-1000], targetSum=-1000` | `True` | 制約の下限値 -1000 で正しく動くか | +| 10 | **Example 2(全パス不一致)** | `root=[1,2,3], targetSum=5` | `False` | すべてのパスを探索し終えたあと `False` を返せるか | + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件の入力 +> - **境界値**:制約の上限・下限にあたる値。今回は `val=1000` や `targetSum=-1000` など +> - **AttributeError**:存在しない属性(プロパティ)にアクセスしようとしたときに発生するエラー。`None.val` などで起きる +> - **再帰深度制限**:Python のデフォルトで約1000。`sys.getrecursionlimit()` で確認できる + +--- + +

FAQ

+ +💡 FAQは初学者がつまずきやすいポイントをQ&A形式でまとめたものです。「なぜその方法を選んだのか」「別の方法ではダメなのか」を中心に、**結論 → 理由 → 具体例**の順で答えています。 + +--- + +**Q1. なぜ `targetSum` を引き算で管理するのですか?加算して比較ではダメですか?** + +**結論**:どちらでも正しく動きますが、引き算の方がコードがシンプルになるため採用しています。 + +**理由**:加算方式だと「現在のパスの合計」を別変数に保持する必要があります。引き算方式では `targetSum` 自体を「残り目標値」として使いまわせるので、変数が1つ少なくて済みます。 + +**具体例**: + +```python +# 加算方式(current_sum を別に管理する必要がある) +def hasPathSum(self, root, targetSum, current_sum=0): + current_sum += root.val + if is_leaf: + return current_sum == targetSum + +# 引き算方式(targetSum 1つで管理できる) +def hasPathSum(self, root, targetSum): + targetSum -= root.val + if is_leaf: + return targetSum == 0 # 残りがゼロかどうかだけ見ればよい +``` + +--- + +**Q2. なぜ「葉」の判定が必要なのですか?`root is None` だけではダメですか?** + +**結論**:ダメです。葉の判定なしだと、内部ノード(子を持つノード)で誤って答えを確定してしまいます。 + +**理由**:`root is None` だけに頼ると、葉ノードの「存在しない左の子(None)」を再帰呼び出しした時点で `False` が返ってしまいます。これでは「葉でちょうど合計が一致した」ケースを正しく捉えられません。 + +**具体例**: + +``` +木: [1], targetSum=1 + +葉の判定あり: + hasPathSum(Node(1), 1) → remaining=0 → 葉 → 0==0 → True ✅ + +葉の判定なし(None チェックのみ): + hasPathSum(Node(1), 1) → remaining=0 + → hasPathSum(None, 0) → return False ❌(誤り) + → hasPathSum(None, 0) → return False ❌(誤り) + → False or False = False ❌ +``` + +--- + +**Q3. 業務版でなぜ `deque` を使うのですか?`list` でも動きませんか?** + +**結論**:末尾操作(`append`/`pop`)だけなら `list` でも動きます。ただし `deque` を使う方が意図が明確で保守性が高まります。 + +**理由**:Python の `list` は末尾への追加・削除は O(1) なので、スタックとして使う分には速度差がほぼありません。しかし `deque` は「スタック(後入れ先出し)またはキュー(先入れ先出し)として使うデータ構造」という意味が明確なため、コードを後から読んだ人が「このデータ構造はスタックとして使っている」とすぐ理解できます。 + +**補足**:仮に「幅優先探索(キュー)に変更したい」という要件変更があった場合、`list` ベースのコードでは `pop(0)` に変えると O(n) になってしまいます。`deque` なら `popleft()` に変えるだけで O(1) のまま済みます。 + +--- + +**Q4. 競技版の再帰で最大5000ノードの木を処理するとスタックオーバーフローになりますか?** + +**結論**:一直線の木(5000ノードが一本の鎖のような形)の場合、Python のデフォルト再帰制限(約1000)を超える可能性があります。 + +**理由**:Python の `sys.getrecursionlimit()` のデフォルト値は約1000です。一直線の木(右の子だけが続く形)では再帰深度がノード数と同じになり、5000ノードで制限を超えます。 + +**対策**: + +- 競技版でも一応 `import sys; sys.setrecursionlimit(10000)` を先頭に追加すれば回避できます +- 根本的な解決策は業務版の「反復DFS + deque」です。`deque` のサイズ制限は Python のヒープメモリ上限(通常数GB)なので、5000ノード程度では問題になりません + +--- + +**Q5. BFS(幅優先探索)を使わないのはなぜですか?** + +**結論**:BFS はこの問題に適していません。なぜなら BFS は「階層ごとに横に広がる」探索で、「根から葉への1本道を追う」パス探索との相性が悪いからです。 + +**理由**:BFS では各ノードを訪問するとき「ここまでの合計」を一緒に管理する必要があります(キューに `(ノード, 合計)` のペアを積む)。これは DFS の実装と同じくらいのコードになり、かつ BFS は最悪ケースで最大幅分のメモリを使うため、DFS に対して不利です。 + +**補足**:BFS が向く場面は「最短経路(最少ステップ)」を求める問題です。今回はパスの合計を比較するだけなので、BFS の「最短」という特性が活きません。 + +--- + +> 📖 **この章で登場した用語** +> +> - **スタックオーバーフロー**:再帰が深くなりすぎてコールスタックの上限を超えるエラー。Pythonでは `RecursionError` として発生する +> - **`sys.setrecursionlimit`**:Pythonの再帰深度の上限を変更する関数。競技環境で一時的に上限を増やすために使う +> - **BFS(幅優先探索 / Breadth-First Search)**:木を階層ごとに横向きに探索する方法。最短経路探索に向くが、今回のようなパス合計問題には DFS が適している +> - **FAQ**:Frequently Asked Questions の略。よくある質問と回答のこと diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html new file mode 100644 index 0000000..8a45f32 --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html @@ -0,0 +1,1340 @@ + + + + + + LeetCode 112 – Path Sum | 再帰DFS解説 + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+
+

+ 💡 この問題を一言で言うと:「根から葉まで下りるルートの数値の合計が + targetSum と一致するパスが1本でも存在するか?」を判定する問題です。 +

+

+ 木(ツリー)は配列と違い、インデックスで直接アクセスできません。根から分岐を辿りながら合計を積み上げ、葉(末端)に到達した瞬間に目標値と比較する必要があります。 +

+
+
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 木には「インデックス」がないため、すべてのパスを順番に辿って確認するしかありません。 +
  • +
  • + 「葉の定義」(左右両方の子が + None)を正確に判定しないと、木の途中のノードで誤って答えを確定してしまいます。 +
  • +
  • + 値が負の場合もあるため、「合計が targetSum + を超えたら打ち切り」などの最適化が使えません。 +
  • +
+
+
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量
+
+
+
+ 再帰DFS +
+
アルゴリズム
+
+
+
+ 0〜5000 +
+
ノード数制約
+
+
+

入出力例

+
+
+
Example 1
+
+ root = [5,4,8,11,null,13,4,7,2,null,null,null,1] +
+
targetSum = 22
+
✅ true
+
+ 5→4→11→2 の合計が 22 になるから +
+
+
+
Example 2
+
+ root = [1,2,3]
targetSum = 5 +
+
❌ false
+
+ 1→2=3、1→3=4 どちらも5にならないから +
+
+
+
Example 3
+
+ root = []
targetSum = 0 +
+
❌ false
+
+ 木が空なのでパス自体が存在しないから +
+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 例:root = [5,4,8,11,null,13,4,7,2,...], targetSum = 22 を使って解説します。 +

+
+
+ + +
+

+ Python 実装 +

+
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. root が None かを確認する(空の木 or 葉を超えた場合)→ False を返す
  2. +
  3. targetSum から現在のノードの値を引いて「残り目標値」を計算する
  4. +
  5. + 葉(左右の子が両方 None)に到達したら、残り目標値が 0 + かどうかで答えを確定する +
  6. +
  7. + 葉でなければ左・右の子に対して同じ処理を再帰的に呼び出し、どちらかが + True なら True を返す +
  8. +
+
+

+ 競技プログラミング版(再帰DFS) +

+
# LeetCode 112 – Path Sum(競技版:再帰DFS)
+# Time: O(n)  Space: O(h)   n=ノード数, h=木の高さ
+
+class Solution(object):
+    def hasPathSum(self, root, targetSum):
+        """
+        :type root: Optional[TreeNode]
+        :type targetSum: int
+        :rtype: bool
+        """
+        # ベースケース①: root が None = 空の木 or 葉を超えた
+        # パスが存在しないので即 False を返す
+        if root is None:
+            return False
+
+        # 現在ノードの値を targetSum から引く
+        # 「残りあとどれだけ合計が必要か」を次の階層に引き継ぐ
+        # 例) targetSum=22, root.val=5 → 次は 17 を目標にする
+        targetSum -= root.val
+
+        # ベースケース②: 葉(左右両方の子が None)に到達した
+        # パスの終点なので、残り目標値がちょうど 0 か確認する
+        if root.left is None and root.right is None:
+            return targetSum == 0
+
+        # 再帰ステップ: 左または右の子ツリーで条件を満たすパスがあれば True
+        # 「or」は短絡評価: 左が True なら右を評価せずに即 True を返す
+        return (self.hasPathSum(root.left, targetSum) or
+                self.hasPathSum(root.right, targetSum))
+
+
+

+ ▶ 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 での動作トレース +

+
+hasPathSum(Node(5),  22) → targetSum = 22-5  = 17  → 葉でない → 左へ
+  hasPathSum(Node(4),  17) → targetSum = 17-4  = 13  → 葉でない → 左へ
+    hasPathSum(Node(11), 13) → targetSum = 13-11 =  2  → 葉でない → 左へ
+      hasPathSum(Node(7),   2) → targetSum =  2-7  = -5  → 葉!→ -5==0? → False ❌
+      hasPathSum(Node(2),   2) → targetSum =  2-2  =  0  → 葉!→  0==0? → True  ✅
+    ← True が伝播
+  ← True が伝播
+← True が伝播
+
+最終出力: True(パス 5→4→11→2, 合計=22)
+
+

+ 業務開発版(反復DFS + deque) +

+
+ ⚠️ Python のデフォルト再帰制限は約 1000 + です。5000ノードの一直線の木では超過する可能性があります。業務版は + deque + を使って再帰なしで安全に実装します。 +
+
from collections import deque
+
+class Solution:
+    def hasPathSum(self, root, targetSum):
+        if root is None:
+            return False
+
+        stack = deque([(root, targetSum)])
+
+        while stack:
+            node, remaining = stack.pop()
+            remaining -= node.val
+
+            if node.left is None and node.right is None:
+                if remaining == 0:
+                    return True
+                continue
+
+            if node.right is not None:
+                stack.append((node.right, remaining))
+            if node.left is not None:
+                stack.append((node.left, remaining))
+
+        return False
+
+
+ + +
+

+ 処理フローチャート +

+ + +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円 + + + 楕円(緑/赤)= 開始・終了 +
+
+ + + + 四角 + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形 + + + ひし形(黄)= 条件分岐 +
+
+ + + + + + 再帰 + + + 二重縦線(紫)= 再帰呼び出し +
+
+ 緑矢印=はい / 成功 + 赤矢印=いいえ / False + 紫矢印=再帰 +
+
+ +
+

+ ✅ return False の出口は1か所に統合しています(② and ⑤ + の両方が合流) +

+

+ ✅ + 「はい」「いいえ」ラベルは分岐点の直隣に配置し、視線移動を最小化しています +

+

+ ✅ + 再帰呼び出し⑥⑦ は二重縦線ボックスで通常の処理と区別しています +

+
+
+ + +
+
+%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13.5px', 'lineColor': '#64748b', 'primaryBorderColor': '#2563eb', 'tertiaryColor': '#ede9fe'}}}%%
+flowchart TD
+    A(["① 開始\nhasPathSum( root, targetSum )"]):::start
+    A --> B{"② root is None?\n空の木・存在しない子"}:::cond
+    B -- "はい" --> F(["return False ❌\n【共通の False 出口】\n② と ⑤ の両方がここへ合流"]):::falseNode
+    B -- "いいえ" --> C["③ remaining = targetSum − root.val\n例: 22 − 5 = 17"]:::proc
+    C --> D{"④ 葉ノードか?\nleft is None  かつ  right is None"}:::cond
+    D -- "はい(葉に到達)" --> E{"⑤ remaining == 0?\n合計がぴったり一致したか"}:::cond
+    E -- "はい" --> T(["return True ✅\nパスが見つかった!"]):::trueNode
+    E -- "いいえ" --> F
+    D -- "いいえ(葉でない)" --> G[["⑥ hasPathSum( root.left,  remaining )\n左の子ツリーへ再帰\nTrue なら即 True(短絡評価 or)"]]:::rec
+    G --> H[["⑦ hasPathSum( root.right, remaining )\n右の子ツリーへ再帰\nleft or right の結果を返す"]]:::rec
+    H --> R(["⑧ 結果を上の階層へ返す\nleft_result  or  right_result"]):::result
+
+    classDef start    fill:#d1fae5,stroke:#059669,stroke-width:2.5px,color:#065f46,font-weight:bold
+    classDef cond     fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#78350f
+    classDef proc     fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
+    classDef falseNode fill:#fee2e2,stroke:#dc2626,stroke-width:2.5px,color:#7f1d1d,font-weight:bold
+    classDef trueNode  fill:#dcfce7,stroke:#16a34a,stroke-width:2.5px,color:#14532d,font-weight:bold
+    classDef rec      fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#3b0764
+    classDef result   fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b
+
+    linkStyle 1 stroke:#dc2626,stroke-width:2.5px,color:#dc2626
+    linkStyle 2 stroke:#16a34a,stroke-width:2px
+    linkStyle 4 stroke:#16a34a,stroke-width:2px
+    linkStyle 5 stroke:#16a34a,stroke-width:2.5px
+    linkStyle 6 stroke:#dc2626,stroke-width:2.5px
+    linkStyle 7 stroke:#7c3aed,stroke-width:2px
+    linkStyle 8 stroke:#7c3aed,stroke-width:2px
+    linkStyle 9 stroke:#64748b,stroke-width:2px
+    
+
+ + +
+

+ 🔎 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 でのフロー追跡 +

+
    +
  1. 「① 開始」→ root=Node(5), targetSum=22 を受け取る
  2. +
  3. + 「② root is None?」→ Node(5) ≠ None → + いいえ(緑矢印で③へ) +
  4. +
  5. 「③ remaining 計算」→ remaining = 22 − 5 = 17
  6. +
  7. + 「④ 葉?」→ left=Node(4), right=Node(8) → + いいえ(葉でない)→ 紫矢印で⑥へ +
  8. +
  9. + 「⑥ 左再帰」→ hasPathSum(Node(4), 17) → … → hasPathSum(Node(2), 2) + へ深潜り +
  10. +
  11. + 「④ 葉?」→ Node(2) は葉 → はい → 「⑤ remaining==0?」→ + 2−2=0 → はい → ✅ return True +
  12. +
  13. True が ⑥ or ⑦ を通じて上の階層へ次々と伝播 → 最終的に True を返す
  14. +
+
+ + +
+

+ 🔴 return False の2つの経路について +

+

+ ② → False:root が + None(空の木・存在しない子ノードを辿った)場合。パス自体が存在しない。
+ ⑤ → False:葉に到達したが残り目標値が 0 + でない場合。このパスの合計は targetSum と一致しない。
+ 両経路とも「このパスに答えはない」という同じ意味を持つため、図では1つの出口ノードに合流させています。 +

+
+
+ + +
+

+ 計算量分析 +

+
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(log n)
+
+ 対数的に増加
例:均衡木の高さ +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 項目 + + 競技版(再帰) + + 業務版(反復deque) +
+ 時間計算量 + + O(n) + + O(n) +
+ 空間計算量 + + O(h) + + O(h) +
+ 均衡木の空間 + + O(log n) + + O(log n) +
+ 最悪(一直線) + + O(n) ⚠️再帰制限 + + O(n) ✅安全 +
+ 可読性 + + ★★★ 高い + + ★★☆ 中程度 +
+
+
+

+ 🔍 なぜこの計算量になるのか +

+

+ 時間計算量 O(n):すべてのノードをちょうど1回ずつ訪問するためです。最良ケース(根が葉)でも最悪ケース(全ノードを見る)でも、訪問回数は必ずノード数 + n 以下に収まります。
+ 空間計算量 O(h):再帰呼び出しのたびに関数の情報がコールスタックに積まれます。その最大の深さが木の高さ + h です。均衡した木では h ≈ log₂(n)(5000ノードで約12段)、一直線の木では h = + n(最悪5000段)になります。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語を五十音順でまとめました。分からない言葉が出てきたときに参照してください。 +

+
+
+ + O(h)(スペース計算量) + +
+ h + は木の高さ(Height)を表します。再帰呼び出しでは関数の呼び出し情報がスタックに積まれ、その深さが木の高さに比例します。均衡した木では + O(log n)、一直線の木では O(n) になります。 +
+
+
+ + DFS(深さ優先探索) + +
+ Depth-First Search + の略。木やグラフをできるだけ深く潜ってから引き返す探索方法です。迷路を一本道ずつ試すイメージです。根から葉への「パス」を追うのに向いています。BFS(幅優先探索)が横に広がるのに対して、DFS + は縦に深く進みます。 +
+
+
+ + + collections.deque(デック) + +
+ Python + の標準ライブラリが提供するデータ構造で「両端開きの箱」のイメージです。前後どちらからでも + O(1) で追加・取り出しができます。list + は先頭への追加・削除が O(n) かかりますが、deque + は O(1) です。スタックやキューとして使うのに最適です。 +
+
+
+ + コールスタック(Call + Stack) + +
+ 関数が呼び出されるたびにその情報(引数・ローカル変数・戻り先)を積み重ねる領域です。再帰関数は呼び出すたびにここに積まれ、返るたびに取り出されます。積みすぎると「スタックオーバーフロー」(Python + では + RecursionError)が発生します。 +
+
+
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す仕組みです。木構造のように「同じ形が入れ子になった」データに対して自然に適用できます。ロシアのマトリョーシカ人形を開くように、同じ操作を繰り返して最終的に「これ以上開けない」(基底条件)に到達します。 +
+
+
+ + 再帰深度制限(Recursion + Limit) + +
+ Python はデフォルトで再帰の深さを約 1000 に制限しています。sys.getrecursionlimit() + で確認できます。5000 + ノードの一直線の木では超過する可能性があるため、業務版では + deque + を使った反復 DFS で回避します。 +
+
+
+ + 短絡評価(Short-circuit + Evaluation) + +
+ A or B + という式で、A が + True なら B + を評価せずに即 + True + を返す仕組みです。今回の実装では左のサブツリーで答えが見つかった場合に右のサブツリー全体の探索をスキップできるため、最良ケースで処理を大幅に短縮できます。 +
+
+
+ + 葉(Leaf Node) + +
+ 木において、左の子も右の子も存在しない末端のノードのことです。根から葉まで下りる一本道が「パス」であり、問題の条件は葉に到達したときにのみ確認します。葉でないノードで条件を確認してしまうと、途中で誤って答えを確定してしまうバグが起きます。 +
+
+
+ + ベースケース(Base + Case) + +
+ 再帰関数において「これ以上再帰呼び出しをしない」と判断して直接値を返す条件のことです。今回は「root + is None」(空の木 or + 存在しない子)と「葉ノードに到達した」の2つがベースケースです。ベースケースがないと関数が無限に呼ばれ続けてクラッシュします。 +
+
+
+
+ + +
+ + + + + + + + + + diff --git a/public/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html b/public/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html new file mode 100644 index 0000000..998fcae --- /dev/null +++ b/public/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html @@ -0,0 +1,1340 @@ + + + + + + LeetCode 112 – Path Sum | 再帰DFS解説 + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+
+

+ 💡 この問題を一言で言うと:「根から葉まで下りるルートの数値の合計が + targetSum と一致するパスが1本でも存在するか?」を判定する問題です。 +

+

+ 木(ツリー)は配列と違い、インデックスで直接アクセスできません。根から分岐を辿りながら合計を積み上げ、葉(末端)に到達した瞬間に目標値と比較する必要があります。 +

+
+
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 木には「インデックス」がないため、すべてのパスを順番に辿って確認するしかありません。 +
  • +
  • + 「葉の定義」(左右両方の子が + None)を正確に判定しないと、木の途中のノードで誤って答えを確定してしまいます。 +
  • +
  • + 値が負の場合もあるため、「合計が targetSum + を超えたら打ち切り」などの最適化が使えません。 +
  • +
+
+
+
+
O(n)
+
時間計算量
+
+
+
O(h)
+
空間計算量
+
+
+
+ 再帰DFS +
+
アルゴリズム
+
+
+
+ 0〜5000 +
+
ノード数制約
+
+
+

入出力例

+
+
+
Example 1
+
+ root = [5,4,8,11,null,13,4,7,2,null,null,null,1] +
+
targetSum = 22
+
✅ true
+
+ 5→4→11→2 の合計が 22 になるから +
+
+
+
Example 2
+
+ root = [1,2,3]
targetSum = 5 +
+
❌ false
+
+ 1→2=3、1→3=4 どちらも5にならないから +
+
+
+
Example 3
+
+ root = []
targetSum = 0 +
+
❌ false
+
+ 木が空なのでパス自体が存在しないから +
+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 例:root = [5,4,8,11,null,13,4,7,2,...], targetSum = 22 を使って解説します。 +

+
+
+ + +
+

+ Python 実装 +

+
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. root が None かを確認する(空の木 or 葉を超えた場合)→ False を返す
  2. +
  3. targetSum から現在のノードの値を引いて「残り目標値」を計算する
  4. +
  5. + 葉(左右の子が両方 None)に到達したら、残り目標値が 0 + かどうかで答えを確定する +
  6. +
  7. + 葉でなければ左・右の子に対して同じ処理を再帰的に呼び出し、どちらかが + True なら True を返す +
  8. +
+
+

+ 競技プログラミング版(再帰DFS) +

+
# LeetCode 112 – Path Sum(競技版:再帰DFS)
+# Time: O(n)  Space: O(h)   n=ノード数, h=木の高さ
+
+class Solution(object):
+    def hasPathSum(self, root, targetSum):
+        """
+        :type root: Optional[TreeNode]
+        :type targetSum: int
+        :rtype: bool
+        """
+        # ベースケース①: root が None = 空の木 or 葉を超えた
+        # パスが存在しないので即 False を返す
+        if root is None:
+            return False
+
+        # 現在ノードの値を targetSum から引く
+        # 「残りあとどれだけ合計が必要か」を次の階層に引き継ぐ
+        # 例) targetSum=22, root.val=5 → 次は 17 を目標にする
+        targetSum -= root.val
+
+        # ベースケース②: 葉(左右両方の子が None)に到達した
+        # パスの終点なので、残り目標値がちょうど 0 か確認する
+        if root.left is None and root.right is None:
+            return targetSum == 0
+
+        # 再帰ステップ: 左または右の子ツリーで条件を満たすパスがあれば True
+        # 「or」は短絡評価: 左が True なら右を評価せずに即 True を返す
+        return (self.hasPathSum(root.left, targetSum) or
+                self.hasPathSum(root.right, targetSum))
+
+
+

+ ▶ 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 での動作トレース +

+
+hasPathSum(Node(5),  22) → targetSum = 22-5  = 17  → 葉でない → 左へ
+  hasPathSum(Node(4),  17) → targetSum = 17-4  = 13  → 葉でない → 左へ
+    hasPathSum(Node(11), 13) → targetSum = 13-11 =  2  → 葉でない → 左へ
+      hasPathSum(Node(7),   2) → targetSum =  2-7  = -5  → 葉!→ -5==0? → False ❌
+      hasPathSum(Node(2),   2) → targetSum =  2-2  =  0  → 葉!→  0==0? → True  ✅
+    ← True が伝播
+  ← True が伝播
+← True が伝播
+
+最終出力: True(パス 5→4→11→2, 合計=22)
+
+

+ 業務開発版(反復DFS + deque) +

+
+ ⚠️ Python のデフォルト再帰制限は約 1000 + です。5000ノードの一直線の木では超過する可能性があります。業務版は + deque + を使って再帰なしで安全に実装します。 +
+
from collections import deque
+
+class Solution:
+    def hasPathSum(self, root, targetSum):
+        if root is None:
+            return False
+
+        stack = deque([(root, targetSum)])
+
+        while stack:
+            node, remaining = stack.pop()
+            remaining -= node.val
+
+            if node.left is None and node.right is None:
+                if remaining == 0:
+                    return True
+                continue
+
+            if node.right is not None:
+                stack.append((node.right, remaining))
+            if node.left is not None:
+                stack.append((node.left, remaining))
+
+        return False
+
+
+ + +
+

+ 処理フローチャート +

+ + +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円 + + + 楕円(緑/赤)= 開始・終了 +
+
+ + + + 四角 + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形 + + + ひし形(黄)= 条件分岐 +
+
+ + + + + + 再帰 + + + 二重縦線(紫)= 再帰呼び出し +
+
+ 緑矢印=はい / 成功 + 赤矢印=いいえ / False + 紫矢印=再帰 +
+
+ +
+

+ ✅ return False の出口は1か所に統合しています(② and ⑤ + の両方が合流) +

+

+ ✅ + 「はい」「いいえ」ラベルは分岐点の直隣に配置し、視線移動を最小化しています +

+

+ ✅ + 再帰呼び出し⑥⑦ は二重縦線ボックスで通常の処理と区別しています +

+
+
+ + +
+
+%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13.5px', 'lineColor': '#64748b', 'primaryBorderColor': '#2563eb', 'tertiaryColor': '#ede9fe'}}}%%
+flowchart TD
+    A(["① 開始\nhasPathSum( root, targetSum )"]):::start
+    A --> B{"② root is None?\n空の木・存在しない子"}:::cond
+    B -- "はい" --> F(["return False ❌\n【共通の False 出口】\n② と ⑤ の両方がここへ合流"]):::falseNode
+    B -- "いいえ" --> C["③ remaining = targetSum − root.val\n例: 22 − 5 = 17"]:::proc
+    C --> D{"④ 葉ノードか?\nleft is None  かつ  right is None"}:::cond
+    D -- "はい(葉に到達)" --> E{"⑤ remaining == 0?\n合計がぴったり一致したか"}:::cond
+    E -- "はい" --> T(["return True ✅\nパスが見つかった!"]):::trueNode
+    E -- "いいえ" --> F
+    D -- "いいえ(葉でない)" --> G[["⑥ hasPathSum( root.left,  remaining )\n左の子ツリーへ再帰\nTrue なら即 True(短絡評価 or)"]]:::rec
+    G --> H[["⑦ hasPathSum( root.right, remaining )\n右の子ツリーへ再帰\nleft or right の結果を返す"]]:::rec
+    H --> R(["⑧ 結果を上の階層へ返す\nleft_result  or  right_result"]):::result
+
+    classDef start    fill:#d1fae5,stroke:#059669,stroke-width:2.5px,color:#065f46,font-weight:bold
+    classDef cond     fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#78350f
+    classDef proc     fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
+    classDef falseNode fill:#fee2e2,stroke:#dc2626,stroke-width:2.5px,color:#7f1d1d,font-weight:bold
+    classDef trueNode  fill:#dcfce7,stroke:#16a34a,stroke-width:2.5px,color:#14532d,font-weight:bold
+    classDef rec      fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#3b0764
+    classDef result   fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b
+
+    linkStyle 1 stroke:#dc2626,stroke-width:2.5px,color:#dc2626
+    linkStyle 2 stroke:#16a34a,stroke-width:2px
+    linkStyle 4 stroke:#16a34a,stroke-width:2px
+    linkStyle 5 stroke:#16a34a,stroke-width:2.5px
+    linkStyle 6 stroke:#dc2626,stroke-width:2.5px
+    linkStyle 7 stroke:#7c3aed,stroke-width:2px
+    linkStyle 8 stroke:#7c3aed,stroke-width:2px
+    linkStyle 9 stroke:#64748b,stroke-width:2px
+    
+
+ + +
+

+ 🔎 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 でのフロー追跡 +

+
    +
  1. 「① 開始」→ root=Node(5), targetSum=22 を受け取る
  2. +
  3. + 「② root is None?」→ Node(5) ≠ None → + いいえ(緑矢印で③へ) +
  4. +
  5. 「③ remaining 計算」→ remaining = 22 − 5 = 17
  6. +
  7. + 「④ 葉?」→ left=Node(4), right=Node(8) → + いいえ(葉でない)→ 紫矢印で⑥へ +
  8. +
  9. + 「⑥ 左再帰」→ hasPathSum(Node(4), 17) → … → hasPathSum(Node(2), 2) + へ深潜り +
  10. +
  11. + 「④ 葉?」→ Node(2) は葉 → はい → 「⑤ remaining==0?」→ + 2−2=0 → はい → ✅ return True +
  12. +
  13. True が ⑥ or ⑦ を通じて上の階層へ次々と伝播 → 最終的に True を返す
  14. +
+
+ + +
+

+ 🔴 return False の2つの経路について +

+

+ ② → False:root が + None(空の木・存在しない子ノードを辿った)場合。パス自体が存在しない。
+ ⑤ → False:葉に到達したが残り目標値が 0 + でない場合。このパスの合計は targetSum と一致しない。
+ 両経路とも「このパスに答えはない」という同じ意味を持つため、図では1つの出口ノードに合流させています。 +

+
+
+ + +
+

+ 計算量分析 +

+
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(log n)
+
+ 対数的に増加
例:均衡木の高さ +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 項目 + + 競技版(再帰) + + 業務版(反復deque) +
+ 時間計算量 + + O(n) + + O(n) +
+ 空間計算量 + + O(h) + + O(h) +
+ 均衡木の空間 + + O(log n) + + O(log n) +
+ 最悪(一直線) + + O(n) ⚠️再帰制限 + + O(n) ✅安全 +
+ 可読性 + + ★★★ 高い + + ★★☆ 中程度 +
+
+
+

+ 🔍 なぜこの計算量になるのか +

+

+ 時間計算量 O(n):すべてのノードをちょうど1回ずつ訪問するためです。最良ケース(根が葉)でも最悪ケース(全ノードを見る)でも、訪問回数は必ずノード数 + n 以下に収まります。
+ 空間計算量 O(h):再帰呼び出しのたびに関数の情報がコールスタックに積まれます。その最大の深さが木の高さ + h です。均衡した木では h ≈ log₂(n)(5000ノードで約12段)、一直線の木では h = + n(最悪5000段)になります。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語を五十音順でまとめました。分からない言葉が出てきたときに参照してください。 +

+
+
+ + O(h)(スペース計算量) + +
+ h + は木の高さ(Height)を表します。再帰呼び出しでは関数の呼び出し情報がスタックに積まれ、その深さが木の高さに比例します。均衡した木では + O(log n)、一直線の木では O(n) になります。 +
+
+
+ + DFS(深さ優先探索) + +
+ Depth-First Search + の略。木やグラフをできるだけ深く潜ってから引き返す探索方法です。迷路を一本道ずつ試すイメージです。根から葉への「パス」を追うのに向いています。BFS(幅優先探索)が横に広がるのに対して、DFS + は縦に深く進みます。 +
+
+
+ + + collections.deque(デック) + +
+ Python + の標準ライブラリが提供するデータ構造で「両端開きの箱」のイメージです。前後どちらからでも + O(1) で追加・取り出しができます。list + は先頭への追加・削除が O(n) かかりますが、deque + は O(1) です。スタックやキューとして使うのに最適です。 +
+
+
+ + コールスタック(Call + Stack) + +
+ 関数が呼び出されるたびにその情報(引数・ローカル変数・戻り先)を積み重ねる領域です。再帰関数は呼び出すたびにここに積まれ、返るたびに取り出されます。積みすぎると「スタックオーバーフロー」(Python + では + RecursionError)が発生します。 +
+
+
+ + 再帰(Recursion) + +
+ 関数が自分自身を呼び出す仕組みです。木構造のように「同じ形が入れ子になった」データに対して自然に適用できます。ロシアのマトリョーシカ人形を開くように、同じ操作を繰り返して最終的に「これ以上開けない」(基底条件)に到達します。 +
+
+
+ + 再帰深度制限(Recursion + Limit) + +
+ Python はデフォルトで再帰の深さを約 1000 に制限しています。sys.getrecursionlimit() + で確認できます。5000 + ノードの一直線の木では超過する可能性があるため、業務版では + deque + を使った反復 DFS で回避します。 +
+
+
+ + 短絡評価(Short-circuit + Evaluation) + +
+ A or B + という式で、A が + True なら B + を評価せずに即 + True + を返す仕組みです。今回の実装では左のサブツリーで答えが見つかった場合に右のサブツリー全体の探索をスキップできるため、最良ケースで処理を大幅に短縮できます。 +
+
+
+ + 葉(Leaf Node) + +
+ 木において、左の子も右の子も存在しない末端のノードのことです。根から葉まで下りる一本道が「パス」であり、問題の条件は葉に到達したときにのみ確認します。葉でないノードで条件を確認してしまうと、途中で誤って答えを確定してしまうバグが起きます。 +
+
+
+ + ベースケース(Base + Case) + +
+ 再帰関数において「これ以上再帰呼び出しをしない」と判断して直接値を返す条件のことです。今回は「root + is None」(空の木 or + 存在しない子)と「葉ノードに到達した」の2つがベースケースです。ベースケースがないと関数が無限に呼ばれ続けてクラッシュします。 +
+
+
+
+ + +
+ + + + + + + + + + diff --git a/public/index.html b/public/index.html index 72c9849..d970233 100644 --- a/public/index.html +++ b/public/index.html @@ -416,7 +416,7 @@

🧪 Algorithm Study Index

-

176 interactive lessons across 6 domains

+

177 interactive lessons across 6 domains

@@ -431,9 +431,9 @@

- + @@ -475,6 +475,7 @@

  • 🧩LeetCode 108 - 昇順配列を高さ平衡BSTに変換Algorithm/BinarySearch/leetcode/108. Convert Sorted Array to Binary Search Tree/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 110 · Balanced Binary TreeAlgorithm/BinaryTree/leetcode/110. Balanced Binary Tree/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 111 - Minimum Depth of Binary Tree | BFS解説Algorithm/BinaryTree/leetcode/111. Minimum Depth of Binary Tree/claude sonnet 4.6 adaptive/README_react.html
  • +
  • 🧩LeetCode 112 – Path Sum | 再帰DFS解説Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 5: Longest Palindromic Substring - 中心展開法Algorithm/ExpandAroundCenter/leetcode/5. Longest Palindromic Substring/Claude/README.html
  • 🧩LeetCode 66: Plus One - 右から左への繰り上がり処理Algorithm/Other/leetcode/66. Plus One/Claude Sonnet 4.5/README_react.html
  • 🧩LeetCode 67: Add Binary - 二進数加算Algorithm/TwoPointers/leetcode/67. Add Binary/Claude/README_react.html
  • @@ -658,6 +659,7 @@

  • 🧩LeetCode 108 - 昇順配列を高さ平衡BSTに変換Algorithm/BinarySearch/leetcode/108. Convert Sorted Array to Binary Search Tree/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 110 · Balanced Binary TreeAlgorithm/BinaryTree/leetcode/110. Balanced Binary Tree/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 111 - Minimum Depth of Binary Tree | BFS解説Algorithm/BinaryTree/leetcode/111. Minimum Depth of Binary Tree/claude sonnet 4.6 adaptive/README_react.html
  • +
  • 🧩LeetCode 112 – Path Sum | 再帰DFS解説Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 5: Longest Palindromic Substring - 中心展開法Algorithm/ExpandAroundCenter/leetcode/5. Longest Palindromic Substring/Claude/README.html
  • 🧩LeetCode 66: Plus One - 右から左への繰り上がり処理Algorithm/Other/leetcode/66. Plus One/Claude Sonnet 4.5/README_react.html
  • 🧩LeetCode 67: Add Binary - 二進数加算Algorithm/TwoPointers/leetcode/67. Add Binary/Claude/README_react.html
  • @@ -835,7 +837,7 @@

    🧪 - Generated on 2026-05-10 + Generated on 2026-05-12