diff --git a/.markdownlint.json b/.markdownlint.json index b21c73fe..f1d9c561 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,11 +1,55 @@ { "default": true, + "MD001": false, + "MD002": true, + "MD003": { "style": "atx" }, + "MD004": { "style": "consistent" }, + "MD007": { "indent": 4 }, + "MD009": { "br_spaces": 2 }, + "MD012": true, "MD013": { - "line_length": 100, + "line_length": 1000, "code_blocks": false, - "tables": false + "tables": false, + "headings": false, + "strict": false }, + "MD018": false, + "MD019": false, + "MD022": true, + "MD024": { "siblings_only": true }, + "MD025": { "front_matter_title": "" }, + "MD026": { "punctuation": ".,;:!。,;:" }, + "MD029": { "style": "ordered" }, + "MD030": { + "ul_single": 1, + "ol_single": 1, + "ul_multi": 1, + "ol_multi": 1 + }, + "MD031": true, + "MD032": true, "MD033": { - "allowed_elements": ["h1", "h2", "details", "summary", "p", "i", "footer", "div"] - } + "allowed_elements": [ + "h1", + "h2", + "p", + "i", + "footer", + "br", + "div", + "sub", + "sup", + "kbd", + "details", + "summary" + ] + }, + "MD034": false, + "MD038": true, + "MD040": false, + "MD041": { "level": 1 }, + "MD042": false, + "MD046": { "style": "fenced" }, + "MD048": { "style": "backtick" } } diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README.md b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README.md new file mode 100644 index 00000000..6d8efb03 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README.md @@ -0,0 +1,337 @@ +# Unique Binary Search Trees - カタラン数による計算 + +

目次

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

概要

+ +**問題**: 1からnまでの連続整数をすべて使って構成できる「構造的に異なるBST(二分探索木)」の個数を求める。 + +**要件**: + +- 制約: `1 <= n <= 19` +- 各ノードの値は1〜nの範囲でユニーク +- 構造が異なればカウント(値の配置ではなく木の形状で判定) + +**数学的背景**: + +- この問題は **n番目のカタラン数 Cₙ** を求める問題に帰着される +- カタラン数の定義: + - `C₀ = 1` + - `Cₙ = Σ(i=0 to n-1) Cᵢ * Cₙ₋₁₋ᵢ` (DP的定義) + - `Cₙ = Cₙ₋₁ * 2(2n - 1) / (n + 1)` (漸化式) + +--- + +

アルゴリズム要点(TL;DR)

+ +- **戦略**: カタラン数の漸化式を利用 +- **データ構造**: スカラー整数変数のみ(配列・再帰不要) +- **時間計算量**: O(n) +- **空間計算量**: O(1) +- **メモリ実測**: 17MB台(LeetCode上位50〜80%) +- **実行速度**: 0ms(100%) + +**漸化式**: + +``` +C₀ = 1 +Cₙ = Cₙ₋₁ * 2(2n - 1) / (n + 1) +``` + +--- + +

図解

+ +### フローチャート + +```mermaid +flowchart TD + Start[Start numTrees n] --> Init[Initialize c equals 1] + Init --> Loop{i from 1 to n} + Loop -- i in range --> Calc[Update c equals c times 2 times 2i minus 1 div i plus 1] + Calc --> Next[Increment i] + Next --> Loop + Loop -- Done --> Return[Return c] +``` + +**説明**: + +- `c` を1で初期化(C₀ = 1) +- iを1からnまでループし、各ステップで漸化式を適用 +- 整数除算 `//` を使うことでPythonの任意精度整数でも正確に計算 + +### データフロー図 + +```mermaid +graph LR + subgraph Input_validation + A[Input n] --> B[Check 1 le n le 19] + end + subgraph Core_computation + B --> C[Initialize c equals 1] + C --> D[Loop i equals 1 to n] + D --> E[Apply recurrence formula] + end + E --> F[Output count] +``` + +**説明**: + +- 入力検証(業務版のみ) +- コア計算:ループ内で漸化式を逐次適用 +- 最終的なカウントを出力 + +--- + +

正しさのスケッチ

+ +**不変条件**: + +- ループのi回目終了時、変数 `c` は `Cᵢ`(i個のノードでのBST数)を保持 + +**基底条件**: + +- `C₀ = 1`: 0個のノードでは空木が1通り(ループ前の初期値) + +**帰納ステップ**: + +- `Cᵢ` が正しいと仮定すると、漸化式 `Cᵢ₊₁ = Cᵢ * 2(2(i+1) - 1) / ((i+1) + 1)` により `Cᵢ₊₁` も正しい +- Pythonの整数除算 `//` は常に正確な整数結果を返すため、誤差は発生しない + +**終了性**: + +- ループは `range(1, n+1)` で必ず有限回で終了 + +**網羅性**: + +- カタラン数の定義により、すべての構造的に異なるBSTが正確にカウントされる + +--- + +

計算量

+ +| 項目 | 計算量 | 備考 | +| ---- | -------- | ----------------------------------------- | +| 時間 | **O(n)** | ループがn回実行され、各ステップは定数時間 | +| 空間 | **O(1)** | 変数c、i のみ使用(配列不要) | + +**比較表(他アプローチとの対比)**: + +| アプローチ | 時間 | 空間 | 可読性 | 備考 | +| ---------------- | ----- | ---- | ------ | -------------------------- | +| 漸化式(本実装) | O(n) | O(1) | ★★★ | 最もシンプルで高速 | +| DP配列 | O(n²) | O(n) | ★★★ | 定義に忠実だが遅い | +| 再帰+メモ化 | O(n²) | O(n) | ★★☆ | 関数呼び出しオーバーヘッド | + +--- + +

Python 実装

+ +```python +from __future__ import annotations + +from typing import Final + + +class Solution: + """ + Unique Binary Search Trees 問題を解くクラス。 + + カタラン数の漸化式を用いてO(n)/O(1)で計算。 + """ + + # 制約を定数として明示 + _MIN_N: Final[int] = 1 + _MAX_N: Final[int] = 19 + + def numTrees(self, n: int) -> int: + """ + LeetCode用エントリポイント(競技プログラミング向け実装)。 + + Args: + n: ノード数 (1 <= n <= 19) + + Returns: + 構造的に異なるBSTの個数 + + Complexity: + Time: O(n) + Space: O(1) + """ + # カタラン数の漸化式: C_0 = 1 + c: int = 1 + + # C_n = C_{n-1} * 2(2n - 1) / (n + 1) + for i in range(1, n + 1): + # 整数除算で誤差なく計算 + c = c * 2 * (2 * i - 1) // (i + 1) + + return c + + # ==== 業務開発向け実装(入力検証付き) ==== + + def numTrees_production(self, n: int) -> int: + """ + 業務開発向けの防御的実装。 + + 入力の型チェック・値チェックを行い、 + 異常値の場合はTypeError/ValueErrorを送出。 + + Args: + n: ノード数 + + Returns: + 構造的に異なるBSTの個数 + + Raises: + TypeError: nがintでない、またはboolである場合 + ValueError: nが有限整数でない、あるいは許容範囲外の場合 + + Complexity: + Time: O(n) + Space: O(1) + """ + # --- 入力検証 --- + if not isinstance(n, int) or isinstance(n, bool): + # boolはintのサブクラスなので明示的に除外 + raise TypeError("n must be an integer") + + if n < self._MIN_N or n > self._MAX_N: + raise ValueError( + f"n must be between {self._MIN_N} and {self._MAX_N}, got {n}" + ) + + # --- メインロジック(カタラン数の漸化式) --- + c: int = 1 + + for i in range(1, n + 1): + # 整数演算のみで誤差は発生しない + c = c * 2 * (2 * i - 1) // (i + 1) + + return c +``` + +**主要ステップ**: + +1. **初期化**: `c = 1`(C₀の値) +2. **ループ**: i = 1 から n まで漸化式を適用 +3. **更新**: `c = c * 2 * (2*i - 1) // (i + 1)` +4. **返却**: 最終的な `c` がCₙ + +**型注釈**: + +- `from __future__ import annotations` で前方参照型も安全 +- Pylanceで型未解決警告なし + +--- + +

CPython最適化ポイント

+ +1. **再帰回避**: + - 再帰を使わずループで実装 → 関数呼び出しオーバーヘッド削減 + - スタックフレーム生成コストゼロ + +2. **ローカル変数の活用**: + - `c` をローカル変数に閉じ込めて更新 + - CPythonではローカル変数アクセスが最速(`LOAD_FAST`/`STORE_FAST`命令) + +3. **整数演算の最適化**: + - `n <= 19` のため、カタラン数C₁₉でも桁数は10桁程度 + - Pythonの任意精度整数でもオーバーヘッドは極小 + +4. **標準ライブラリ不使用**: + - `functools.lru_cache`、`math.comb` などを使わない + - 単純なループのみでCPythonが直接実行しやすい + +5. **メモリアロケーション最小化**: + - 配列・リスト・辞書を一切使わない + - スカラー整数2つ(`c`、`i`)のみ + +**実測結果**: + +- Runtime: 0 ms(100%) +- Memory: 17.61〜17.74 MB(50〜80%) + +--- + +

エッジケースと検証観点

+ +| ケース | 入力 | 期待出力 | 検証観点 | +| -------- | ---- | ---------- | ---------------------------- | +| 最小値 | n=1 | 1 | 基底条件 | +| 小さい値 | n=2 | 2 | 漸化式の初期動作 | +| 中間値 | n=3 | 5 | 一般的なケース | +| 上限値 | n=19 | 1767263190 | 整数精度、オーバーフロー検証 | + +**業務版での異常系**: + +- `n = 0` → `ValueError`(範囲外) +- `n = 20` → `ValueError`(範囲外) +- `n = 1.5` → `TypeError`(非整数) +- `n = True` → `TypeError`(bool除外) + +**正常系の検証**: + +- 各 n に対するカタラン数は既知の数列(OEIS A000108)と一致 +- 整数除算 `//` により誤差ゼロで計算される + +--- + +

FAQ

+ +### Q1: なぜDP配列を使わないのか? + +**A**: + +- カタラン数は漸化式で `Cₙ = f(Cₙ₋₁)` の形で表現できる +- 過去の値を配列で保持する必要がなく、1つ前の値だけあればよい +- メモリ O(1)、速度も O(n²) → O(n) に改善 + +### Q2: 整数除算 `//` でなぜ誤差が出ないのか? + +**A**: + +- カタラン数の漸化式は、数学的に常に整数結果を返すことが保証されている +- `2(2n-1)` が常に `(n+1)` で割り切れる性質を持つ +- Pythonの `//` は厳密な整数除算なので、浮動小数点誤差が発生しない + +### Q3: n=19 でオーバーフローしないのか? + +**A**: + +- C₁₉ = 1767263190(約17億) +- Pythonの `int` は任意精度なので、桁数に制限なし +- C₁₉でも10桁程度なので、メモリ・速度ともに問題なし + +### Q4: さらに高速化できるか? + +**A**: + +- 理論上は O(1) の「定数テーブル」版(n<=19なので20要素の配列をベタ書き) +- ただし、実測ではほぼ差がない(0ms / 100% はすでに限界) +- 可読性とのトレードオフを考えると、現実装が最適 + +### Q5: 業務コードと競技コードの使い分けは? + +**A**: + +- **業務版** (`numTrees_production`): + - 入力検証、型チェック、docstring充実 + - 予期しない入力に対して適切な例外を送出 +- **競技版** (`numTrees`): + - 制約が保証されている前提で検証を省略 + - 最小限のコードで最高速度を追求 + +LeetCodeでは競技版を使用し、実務では業務版を推奨。 diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README_react.html b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README_react.html new file mode 100644 index 00000000..90be2644 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/README_react.html @@ -0,0 +1,702 @@ + + + + + + LeetCode 96: Unique Binary Search Trees - カタラン数解説 + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +

問題の説明

+

+ 整数 n が与えられたとき、 + 1 から n + までの連続整数をすべて使って構成できる「構造的に異なる二分探索木(BST)」の個数を返す問題です。 +

+ +

入出力例

+
+

Example 1:

+
Input: n = 3
+Output: 5
+

+ 解説: n=3 のとき、構造的に異なるBSTは5通り存在します。 +

+
+ +
+

Example 2:

+
Input: n = 1
+Output: 1
+
+ +

制約条件

+ + +

戦略

+ + +

主要ポイント

+ +
+ + +
+

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

+ +
+
+ + +
+

+ Python実装 +

+
class Solution:
+    """
+    Unique Binary Search Trees 問題を解くクラス。
+
+    カタラン数の漸化式を用いてO(n)/O(1)で計算。
+    """
+
+    def numTrees(self, n: int) -> int:
+        """
+        LeetCode用エントリポイント(競技プログラミング向け実装)。
+
+        Args:
+            n: ノード数 (1 <= n <= 19)
+
+        Returns:
+            構造的に異なるBSTの個数
+
+        Complexity:
+            Time: O(n)
+            Space: O(1)
+        """
+        # カタラン数の漸化式: C_0 = 1
+        c: int = 1
+
+        # C_n = C_{n-1} * 2(2n - 1) / (n + 1)
+        for i in range(1, n + 1):
+            # 整数除算で誤差なく計算
+            c = c * 2 * (2 * i - 1) // (i + 1)
+
+        return c
+
+ + +
+

+ フローチャート +

+
+ + + + + + + + + + + + + + + + + + + + 開始 numTrees(n) + + + + + + 初期化 + c = 1 (C₀の値) + + + + + + + + + ループ開始 + i = 1 から n まで + + + + + + + + + i <= n ? + + + + + + + + + 漸化式を適用 + c = c × 2(2i - 1) + ÷ (i + 1) + + + + + + はい + + + + + + i = i + 1 + + + + + + + + + 次の i へ + + + + + + 終了 + return c + + + + + + いいえ + + +
+ +

+ フローの説明:
+ 1. 初期化: c = 1 で開始(C₀の値)
+ 2. ループ開始: i = 1 から n まで反復
+ 3. 条件分岐: i <= n + であれば処理を続行、そうでなければ終了
+ 4. 漸化式適用: c = c × 2(2i - 1) ÷ (i + 1) を計算
+ 5. インクリメント: i を1増やす
+ 6. ループバック: 条件分岐に戻る(紫の矢印)
+ 7. 終了: ループ完了後、c を返す +

+
+ + +
+

+ 計算量分析 +

+ +
+ + + + + + + + + + + + + + + + + + + + +
+ 項目 + + 計算量 + + 備考 +
時間 + O(n) + + ループが n 回実行され、各ステップは定数時間 +
空間 + O(1) + + 変数 c, i のみ使用(配列不要) +
+
+ +

他手法との比較

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ アプローチ + + 時間 + + 空間 + + 備考 +
+ 漸化式(本実装) + + O(n) + + O(1) + + 最もシンプルで高速 +
DP配列O(n²)O(n) + 定義に忠実だが遅い +
再帰+メモ化O(n²)O(n) + 関数呼び出しオーバーヘッド +
+
+
+
+ + + + + + + + + + + + + + + + diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/StepByStepDemo.js b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/StepByStepDemo.js new file mode 100644 index 00000000..b9de0c68 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/StepByStepDemo.js @@ -0,0 +1,397 @@ +"use strict"; +(() => { + const { useState, useEffect, useRef } = React; + const stepsData = [ + { + step: 1, + title: "\u521D\u671F\u5316", + desc: "\u30AB\u30BF\u30E9\u30F3\u6570\u306E\u57FA\u5E95\u6761\u4EF6 C\u2080 = 1 \u3092\u8A2D\u5B9A\u3057\u307E\u3059\u3002\u5909\u6570 c \u3092 1 \u3067\u521D\u671F\u5316\u3057\u307E\u3059\u3002", + visual: { + type: "init", + n: 5, + c: 1, + i: 0 + } + }, + { + step: 2, + title: "\u30EB\u30FC\u30D7\u958B\u59CB (i=1)", + desc: "i = 1 \u304B\u3089 n \u307E\u3067\u53CD\u5FA9\u3092\u958B\u59CB\u3057\u307E\u3059\u3002\u5404\u30B9\u30C6\u30C3\u30D7\u3067\u6F38\u5316\u5F0F\u3092\u9069\u7528\u3057\u3066\u30AB\u30BF\u30E9\u30F3\u6570\u3092\u66F4\u65B0\u3057\u307E\u3059\u3002", + visual: { + type: "loop", + n: 5, + c: 1, + i: 1, + formula: "c = 1 \xD7 2(2\xD71 - 1) \xF7 (1 + 1) = 1 \xD7 2 \xF7 2 = 1" + } + }, + { + step: 3, + title: "i=2: C\u2082\u3092\u8A08\u7B97", + desc: "\u6F38\u5316\u5F0F c = c \xD7 2(2i - 1) \xF7 (i + 1) \u3092\u9069\u7528\u3002c = 1 \xD7 2(4-1) \xF7 3 = 1 \xD7 6 \xF7 3 = 2", + visual: { + type: "loop", + n: 5, + c: 2, + i: 2, + formula: "c = 1 \xD7 2(2\xD72 - 1) \xF7 (2 + 1) = 1 \xD7 6 \xF7 3 = 2" + } + }, + { + step: 4, + title: "i=3: C\u2083\u3092\u8A08\u7B97", + desc: "c = 2 \xD7 2(6-1) \xF7 4 = 2 \xD7 10 \xF7 4 = 5\u30023\u500B\u306E\u30CE\u30FC\u30C9\u3067\u4F5C\u308C\u308BBST\u306F5\u901A\u308A\u3002", + visual: { + type: "loop", + n: 5, + c: 5, + i: 3, + formula: "c = 2 \xD7 2(2\xD73 - 1) \xF7 (3 + 1) = 2 \xD7 10 \xF7 4 = 5" + } + }, + { + step: 5, + title: "i=4: C\u2084\u3092\u8A08\u7B97", + desc: "c = 5 \xD7 2(8-1) \xF7 5 = 5 \xD7 14 \xF7 5 = 14\u30024\u500B\u306E\u30CE\u30FC\u30C9\u3067\u4F5C\u308C\u308BBST\u306F14\u901A\u308A\u3002", + visual: { + type: "loop", + n: 5, + c: 14, + i: 4, + formula: "c = 5 \xD7 2(2\xD74 - 1) \xF7 (4 + 1) = 5 \xD7 14 \xF7 5 = 14" + } + }, + { + step: 6, + title: "i=5: C\u2085\u3092\u8A08\u7B97", + desc: "c = 14 \xD7 2(10-1) \xF7 6 = 14 \xD7 18 \xF7 6 = 42\u30025\u500B\u306E\u30CE\u30FC\u30C9\u3067\u4F5C\u308C\u308BBST\u306F42\u901A\u308A\u3002", + visual: { + type: "loop", + n: 5, + c: 42, + i: 5, + formula: "c = 14 \xD7 2(2\xD75 - 1) \xF7 (5 + 1) = 14 \xD7 18 \xF7 6 = 42" + } + }, + { + step: 7, + title: "\u30EB\u30FC\u30D7\u7D42\u4E86", + desc: "i > n \u3068\u306A\u308A\u30EB\u30FC\u30D7\u7D42\u4E86\u3002\u6700\u7D42\u7684\u306A c \u306E\u5024\uFF08C\u2085 = 42\uFF09\u3092\u8FD4\u3057\u307E\u3059\u3002", + visual: { + type: "result", + n: 5, + c: 42, + i: 6 + } + } + ]; + function StepVisualization({ visual }) { + const { type, n, c, i, formula } = visual; + if (type === "init") { + return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 600 250", className: "step-visualization-svg" }, /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "50", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "18", + fontWeight: "600", + fill: "#0f766e" + }, + "\u521D\u671F\u72B6\u614B: n = ", + n + ), /* @__PURE__ */ React.createElement( + "rect", + { + x: "200", + y: "90", + width: "200", + height: "80", + rx: "8", + fill: "#d1fae5", + stroke: "#10b981", + strokeWidth: "2" + } + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "115", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "18", + fontWeight: "600" + }, + /* @__PURE__ */ React.createElement("tspan", { x: "300", dy: "-8" }, "c = ", c), + /* @__PURE__ */ React.createElement("tspan", { x: "300", dy: "20" }, "i = ", i) + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "200", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "16", + fill: "#64748b" + }, + "C\u2080 = 1 \u3092\u8A2D\u5B9A\uFF08\u30AB\u30BF\u30E9\u30F3\u6570\u306E\u57FA\u5E95\uFF09" + )); + } + if (type === "loop") { + return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 600 350", className: "step-visualization-svg" }, /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "40", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "18", + fontWeight: "600", + fill: "#0f766e" + }, + "\u30B9\u30C6\u30C3\u30D7 i = ", + i, + " / n = ", + n + ), /* @__PURE__ */ React.createElement( + "rect", + { + x: "150", + y: "80", + width: "300", + height: "80", + rx: "8", + fill: "#e0f2fe", + stroke: "#0284c7", + strokeWidth: "2" + } + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "105", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "16", + fontWeight: "600" + }, + /* @__PURE__ */ React.createElement("tspan", { x: "300", dy: "-8" }, "\u73FE\u5728\u306E\u5024: c = ", c), + /* @__PURE__ */ React.createElement("tspan", { x: "300", dy: "20" }, "\u30EB\u30FC\u30D7\u5909\u6570: i = ", i) + ), /* @__PURE__ */ React.createElement( + "rect", + { + x: "50", + y: "190", + width: "500", + height: "60", + rx: "8", + fill: "#fef3c7", + stroke: "#f59e0b", + strokeWidth: "2" + } + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "220", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "14", + fontWeight: "600" + }, + formula + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "280", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "16", + fill: "#64748b" + }, + "\u6F38\u5316\u5F0F\u3092\u9069\u7528\u3057\u3066 C", + i, + " \u3092\u8A08\u7B97" + )); + } + if (type === "result") { + return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 600 250", className: "step-visualization-svg" }, /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "50", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "18", + fontWeight: "600", + fill: "#0f766e" + }, + "\u6700\u7D42\u7D50\u679C" + ), /* @__PURE__ */ React.createElement( + "rect", + { + x: "150", + y: "90", + width: "300", + height: "80", + rx: "8", + fill: "#d1fae5", + stroke: "#10b981", + strokeWidth: "2" + } + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "115", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "18", + fontWeight: "600" + }, + /* @__PURE__ */ React.createElement("tspan", { x: "300", dy: "-8" }, "C", n, " = ", c), + /* @__PURE__ */ React.createElement("tspan", { x: "300", dy: "20" }, "i = ", i, " (\u30EB\u30FC\u30D7\u7D42\u4E86)") + ), /* @__PURE__ */ React.createElement( + "text", + { + x: "300", + y: "200", + textAnchor: "middle", + dominantBaseline: "middle", + fontSize: "16", + fill: "#64748b" + }, + "n = ", + n, + " \u306E\u3068\u304D\u3001", + c, + " \u901A\u308A\u306EBST\u304C\u5B58\u5728" + )); + } + return null; + } + function StepByStepDemo() { + const [activeStep, setActiveStep] = useState(1); + const [isPlaying, setIsPlaying] = useState(false); + const timerRef = useRef(null); + const currentStepData = stepsData.find((s) => s.step === activeStep) || stepsData[0]; + useEffect(() => { + if (isPlaying) { + if (activeStep >= stepsData.length) { + setIsPlaying(false); + setActiveStep(1); + return; + } + timerRef.current = setTimeout(() => { + setActiveStep((prev) => prev + 1); + }, 2e3); + } + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [isPlaying, activeStep]); + const handlePlay = () => { + if (isPlaying) return; + setIsPlaying(true); + if (activeStep >= stepsData.length) { + setActiveStep(1); + } + }; + const handlePrev = () => { + setIsPlaying(false); + setActiveStep((prev) => Math.max(1, prev - 1)); + }; + const handleNext = () => { + setIsPlaying(false); + setActiveStep((prev) => Math.min(stepsData.length, prev + 1)); + }; + const handleReset = () => { + setIsPlaying(false); + setActiveStep(1); + }; + const handleStepClick = (step) => { + setIsPlaying(false); + setActiveStep(step); + }; + return /* @__PURE__ */ React.createElement("div", { className: "grid grid-cols-1 md:grid-cols-[1fr_2fr] gap-8 mt-2" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { className: "mt-0 mb-4 text-teal-800 text-xl font-semibold" }, "\u30B9\u30C6\u30C3\u30D7\u4E00\u89A7"), /* @__PURE__ */ React.createElement("div", { className: "space-y-2" }, stepsData.map((step) => { + const isActive = activeStep === step.step; + return /* @__PURE__ */ React.createElement( + "button", + { + key: step.step, + type: "button", + className: [ + "w-full text-left text-[0.95rem] rounded-xl border-2 transition cursor-pointer px-4 py-4", + "bg-white border-slate-200 hover:border-emerald-500 hover:translate-x-1", + isActive ? "bg-[linear-gradient(135deg,#d1fae5,#a7f3d0)] border-emerald-500 shadow-[0_4px_12px_rgba(16,185,129,0.20)]" : "" + ].join(" "), + onClick: () => handleStepClick(step.step), + "aria-label": `\u30B9\u30C6\u30C3\u30D7${step.step}: ${step.title}`, + "aria-current": isActive ? "step" : void 0 + }, + /* @__PURE__ */ React.createElement( + "div", + { + className: [ + "font-bold mb-1", + isActive ? "text-emerald-900" : "text-teal-800" + ].join(" ") + }, + "Step ", + step.step, + ": ", + step.title + ), + /* @__PURE__ */ React.createElement("div", { className: "text-slate-500 text-sm mt-1" }, step.desc.substring(0, 50), step.desc.length > 50 ? "..." : "") + ); + }))), /* @__PURE__ */ React.createElement("div", { className: "my-auto" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl p-8 mt-10 border-2 border-emerald-200 bg-[linear-gradient(135deg,#ecfdf5,#f0f9ff)]" }, /* @__PURE__ */ React.createElement("h3", { className: "mt-0 text-teal-800 text-xl font-semibold" }, "Step ", currentStepData.step, ": ", currentStepData.title), /* @__PURE__ */ React.createElement("p", { className: "text-slate-600 leading-7" }, currentStepData.desc)), /* @__PURE__ */ React.createElement(StepVisualization, { visual: currentStepData.visual }), /* @__PURE__ */ React.createElement("div", { className: "flex flex-wrap justify-center gap-2 mt-1" }, /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "px-6 py-3 rounded-xl font-semibold text-white transition disabled:opacity-50 shadow hover:-translate-y-0.5 hover:shadow-[0_8px_16px_rgba(16,185,129,0.30)] bg-[linear-gradient(135deg,#10b981,#059669)]", + onClick: handlePlay, + disabled: isPlaying, + "aria-label": "\u81EA\u52D5\u518D\u751F" + }, + "\u25B6 Play" + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "px-6 py-3 rounded-xl font-semibold text-white transition disabled:opacity-50 shadow hover:-translate-y-0.5 hover:shadow-[0_8px_16px_rgba(16,185,129,0.30)] bg-[linear-gradient(135deg,#0ea5e9,#0284c7)]", + onClick: handlePrev, + disabled: activeStep === 1, + "aria-label": "\u524D\u306E\u30B9\u30C6\u30C3\u30D7" + }, + "\u25C0 Prev" + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "px-6 py-3 rounded-xl font-semibold text-white transition disabled:opacity-50 shadow hover:-translate-y-0.5 hover:shadow-[0_8px_16px_rgba(16,185,129,0.30)] bg-[linear-gradient(135deg,#0ea5e9,#0284c7)]", + onClick: handleNext, + disabled: activeStep === stepsData.length, + "aria-label": "\u6B21\u306E\u30B9\u30C6\u30C3\u30D7" + }, + "Next \u25B6" + ), /* @__PURE__ */ React.createElement( + "button", + { + type: "button", + className: "px-6 py-3 rounded-xl font-semibold text-white transition shadow hover:-translate-y-0.5 hover:shadow-[0_8px_16px_rgba(16,185,129,0.30)] bg-[linear-gradient(135deg,#64748b,#475569)]", + onClick: handleReset, + "aria-label": "\u30EA\u30BB\u30C3\u30C8" + }, + "\u21BB Reset" + )))); + } + const container = document.getElementById("step-container"); + if (!container) { + console.error('Element with id "step-container" not found'); + } else { + const root = ReactDOM.createRoot(container); + root.render(/* @__PURE__ */ React.createElement(StepByStepDemo, null)); + } +})(); diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/StepByStepDemo.jsx b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/StepByStepDemo.jsx new file mode 100644 index 00000000..b0cfcd63 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/StepByStepDemo.jsx @@ -0,0 +1,444 @@ +// Update StepByStepDemo.js with: +// npx esbuild StepByStepDemo.jsx --loader:.jsx=jsx --target=es2017 --format=iife --outfile=StepByStepDemo.js +const { useState, useEffect, useRef } = React; + + // ステップデータ + const stepsData = [ + { + step: 1, + title: '初期化', + desc: 'カタラン数の基底条件 C₀ = 1 を設定します。変数 c を 1 で初期化します。', + visual: { + type: 'init', + n: 5, + c: 1, + i: 0, + }, + }, + { + step: 2, + title: 'ループ開始 (i=1)', + desc: 'i = 1 から n まで反復を開始します。各ステップで漸化式を適用してカタラン数を更新します。', + visual: { + type: 'loop', + n: 5, + c: 1, + i: 1, + formula: 'c = 1 × 2(2×1 - 1) ÷ (1 + 1) = 1 × 2 ÷ 2 = 1', + }, + }, + { + step: 3, + title: 'i=2: C₂を計算', + desc: '漸化式 c = c × 2(2i - 1) ÷ (i + 1) を適用。c = 1 × 2(4-1) ÷ 3 = 1 × 6 ÷ 3 = 2', + visual: { + type: 'loop', + n: 5, + c: 2, + i: 2, + formula: 'c = 1 × 2(2×2 - 1) ÷ (2 + 1) = 1 × 6 ÷ 3 = 2', + }, + }, + { + step: 4, + title: 'i=3: C₃を計算', + desc: 'c = 2 × 2(6-1) ÷ 4 = 2 × 10 ÷ 4 = 5。3個のノードで作れるBSTは5通り。', + visual: { + type: 'loop', + n: 5, + c: 5, + i: 3, + formula: 'c = 2 × 2(2×3 - 1) ÷ (3 + 1) = 2 × 10 ÷ 4 = 5', + }, + }, + { + step: 5, + title: 'i=4: C₄を計算', + desc: 'c = 5 × 2(8-1) ÷ 5 = 5 × 14 ÷ 5 = 14。4個のノードで作れるBSTは14通り。', + visual: { + type: 'loop', + n: 5, + c: 14, + i: 4, + formula: 'c = 5 × 2(2×4 - 1) ÷ (4 + 1) = 5 × 14 ÷ 5 = 14', + }, + }, + { + step: 6, + title: 'i=5: C₅を計算', + desc: 'c = 14 × 2(10-1) ÷ 6 = 14 × 18 ÷ 6 = 42。5個のノードで作れるBSTは42通り。', + visual: { + type: 'loop', + n: 5, + c: 42, + i: 5, + formula: 'c = 14 × 2(2×5 - 1) ÷ (5 + 1) = 14 × 18 ÷ 6 = 42', + }, + }, + { + step: 7, + title: 'ループ終了', + desc: 'i > n となりループ終了。最終的な c の値(C₅ = 42)を返します。', + visual: { + type: 'result', + n: 5, + c: 42, + i: 6, + }, + }, + ]; + + // 可視化コンポーネント + function StepVisualization({ visual }) { + const { type, n, c, i, formula } = visual; + + if (type === 'init') { + return ( + + + 初期状態: n = {n} + + + + + + c = {c} + + + i = {i} + + + + + C₀ = 1 を設定(カタラン数の基底) + + + ); + } + + if (type === 'loop') { + return ( + + + ステップ i = {i} / n = {n} + + + {/* 現在の値 */} + + + + 現在の値: c = {c} + + + ループ変数: i = {i} + + + + {/* 計算式 */} + + + {formula} + + + {/* 説明 */} + + 漸化式を適用して C{i} を計算 + + + ); + } + + if (type === 'result') { + return ( + + + 最終結果 + + + + + + C{n} = {c} + + + i = {i} (ループ終了) + + + + + n = {n} のとき、{c} 通りのBSTが存在 + + + ); + } + + return null; + } + + // メインコンポーネント + function StepByStepDemo() { + const [activeStep, setActiveStep] = useState(1); + const [isPlaying, setIsPlaying] = useState(false); + const timerRef = useRef(null); + + const currentStepData = + stepsData.find((s) => s.step === activeStep) || stepsData[0]; + + // 自動再生 + useEffect(() => { + if (isPlaying) { + if (activeStep >= stepsData.length) { + setIsPlaying(false); + setActiveStep(1); + return; + } + timerRef.current = setTimeout(() => { + setActiveStep((prev) => prev + 1); + }, 2000); + } + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [isPlaying, activeStep]); + + const handlePlay = () => { + if (isPlaying) return; + setIsPlaying(true); + if (activeStep >= stepsData.length) { + setActiveStep(1); + } + }; + + const handlePrev = () => { + setIsPlaying(false); + setActiveStep((prev) => Math.max(1, prev - 1)); + }; + + const handleNext = () => { + setIsPlaying(false); + setActiveStep((prev) => Math.min(stepsData.length, prev + 1)); + }; + + const handleReset = () => { + setIsPlaying(false); + setActiveStep(1); + }; + + const handleStepClick = (step) => { + setIsPlaying(false); + setActiveStep(step); + }; + + return ( +
+ {/* 左: ステップリスト */} +
+

+ ステップ一覧 +

+
+ {stepsData.map((step) => { + const isActive = activeStep === step.step; + return ( + + ); + })} +
+
+ + {/* 右: 可視化 + コントロール */} +
+
+

+ Step {currentStepData.step}: {currentStepData.title} +

+

{currentStepData.desc}

+
+ + + +
+ + + + +
+
+
+ ); + } + + // レンダリング + const container = document.getElementById('step-container'); + if (!container) { + console.error('Element with id "step-container" not found'); + } else { + const root = ReactDOM.createRoot(container); + root.render(); + } diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/readme-react.css b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/readme-react.css new file mode 100644 index 00000000..5f82b018 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/claude 4.5 sonnet/readme-react.css @@ -0,0 +1,11 @@ +.flowchart-svg { + max-width: 100%; + height: auto; + color: #333; +} + +.step-visualization-svg { + margin-top: 20px; + max-width: 100%; + height: auto; +} diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/README.md b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/README.md new file mode 100644 index 00000000..eb8585ce --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/README.md @@ -0,0 +1,640 @@ +# Unique Binary Search Trees - カタラン数で数えるBSTの個数 + +## Table of Contents + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython 最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +- **プラットフォーム / ID**: LeetCode 96 +- **タイトル**: Unique Binary Search Trees +- **問題要約**: + - `1..n` の **連続した整数**をそれぞれ一度だけ使って二分探索木(BST)を作る。 + - **構造的に異なる BST がいくつ存在するか**を数える問題。 + +- **入出力仕様(簡潔)**: + - 入力: 整数 `n`(`1 <= n <= 19`) + - 出力: 構造的に異なる BST の数(`int`) + +- **関数シグネチャ(LeetCode準拠)**: + + ```python + class Solution: + def numTrees(self, n: int) -> int: + ... + ``` + +- **代表例**: + - `n = 1` → 1 通り(ノード 1 だけの木) + - `n = 3` → 5 通り + +- **想定データ構造**: + - 実装上は **木構造を実際に構築しない**。 + 必要なのは「通り数」だけなので、内部的には `int` だけで十分。 + +本問題は、数学的には **カタラン数(Catalan number)** を求める典型問題です。 + +--- + +

アルゴリズム要点(TL;DR)

+ +- 各 `n` に対して作れる BST の個数は **n 番目のカタラン数 Cₙ** になる。 +- カタラン数には以下の漸化式がある: + - `C₀ = 1` + - `Cₙ = Cₙ₋₁ * 2 * (2n - 1) / (n + 1)` (n ≥ 1) + +- 実装では **整数演算**で `Cₙ` を順に求める: + - `c = 1` から始めて `n` 回ループし、上記の式で更新。 + +- **データ構造**: + - 配列も木も不要。`int` 変数 `c` だけを更新する。 + +- **計算量(ターゲット)**: + - 時間: **O(n)** + - 空間: **O(1)**(追加メモリは定数) + +- **Python 的ポイント**: + - 任意精度整数によりオーバーフローの心配無し。 + - 再帰無し・ループ1本で CPython に優しい実装。 + +--- + +

図解

+ +### フローチャート(漸化式によるカタラン数計算) + +```mermaid +flowchart TD + Start[Start numTrees] --> Init[Set c = 1] + Init --> LoopCheck{i ≤ n} + LoopCheck -- No --> Ret[Return c] + LoopCheck -- Yes --> Update[Update c with formula] + Update --> Inc[Increment i] + Inc --> LoopCheck +``` + +- `c` がカタラン数 `C₀` から始まり、`n` 回の更新で `Cₙ` に到達する流れを表しています。 + +### データフロー図(入力から出力まで) + +```mermaid +graph LR + subgraph Precheck + In[Input n] --> N1[Assume 1 ≤ n ≤ 19] + end + subgraph Core + N1 --> C0[Init c = 1] + C0 --> Loop[For i from 1 to n] + Loop --> F1[Apply Catalan formula] + F1 --> Cn[Store updated c] + end + Cn --> Out[Output c] +``` + +- 実際には **補助配列や木構造は一切使わず**、`c` という整数をひたすら更新するだけであることを示しています。 + +--- + +

正しさのスケッチ

+ +このアルゴリズムが正しい理由を、カタラン数と BST の対応関係から説明します。 + +### 1. BST の通り数とカタラン数の関係 + +`n` 個の異なるキー(`1..n`)を使って BST を作るとき: + +- どれか 1 つを根に選ぶ(例えば値 `k`)。 +- 左部分木には `1..(k-1)` の要素が入る(要素数 `k-1`)。 +- 右部分木には `(k+1)..n` の要素が入る(要素数 `n-k`)。 +- 左右部分木はそれぞれ「部分問題」とみなせる。 + +ここで、 + +- `G(n)`: `n` 個のキーから作れる BST の通り数 + とすると、 + +- 根を `k` としたときの通り数は: + - 左に `k-1` 個 → `G(k-1)` + - 右に `n-k` 個 → `G(n-k)` + - 左右独立に選べるので、通り数は `G(k-1) * G(n-k)`。 + +- 根 `k` を `1..n` で動かした総和が `G(n)`: + - `G(n) = Σ G(k-1) * G(n-k)` (k = 1..n) + +これはカタラン数の典型的な再帰関係: + +- `C₀ = 1` +- `Cₙ = Σ Cᵢ * Cₙ₋₁₋ᵢ`(i = 0..n-1) + +と完全に一致します。 +したがって `G(n) = Cₙ` です。 + +### 2. 利用している漸化式の正しさ + +カタラン数 `Cₙ` には次の漸化式も知られています: + +- `C₀ = 1` +- `Cₙ = Cₙ₋₁ * 2 * (2n - 1) / (n + 1)` + +これは組合せ論的に + +- `Cₙ = (1 / (n + 1)) * binom(2n, n)` + +から導かれる標準的な式です(詳細証明は組合せ論に譲ります)。 +重要なのは、この式が **整数値を返すことが保証されている** ことです。 + +### 3. 帰納法による正しさ + +1. **基底**: `n = 0` + - 定義上 `C₀ = 1`。 + 「空の木」は 1 通りなので BST の個数と一致する。 + +2. **帰納ステップ**: `n-1` までの BST の通り数が `C₀..Cₙ₋₁` で与えられると仮定。 + - BST の構造分解より `G(n) = Σ G(k-1) * G(n-k)`。 + - 仮定から `G(i) = Cᵢ`(`0 <= i <= n-1`)なので、 + `G(n) = Σ Cᵢ * Cₙ₋₁₋ᵢ = Cₙ`。 + - 一方、実装は `C₀` から漸化式で `Cₙ` を正しく構築する。 + +よってすべての `n`(0〜19)について、実装が返す値は BST の通り数と一致します。 + +### 4. 終了性 + +- ループは `i = 1..n` の有限回で必ず終了。 +- `n <= 19` という問題の制約から、オーバーフロー・無限ループの心配もない。 + +--- + +

計算量

+ +- **時間計算量**: + - ループを `i = 1..n` の 1 回だけ回す → **O(n)**。 + +- **空間計算量**: + - 追加で保持するのは整数 `c` とループ変数 `i` のみ → **O(1)**。 + +比較として、よくある DP 実装との違いを表にすると: + +| アプローチ | 時間計算量 | 空間計算量 | 備考 | +| -------------------------- | ---------- | ---------- | ------------------------------------ | +| 漸化式によるカタラン数計算 | O(n) | O(1) | 本実装。ループ1本、整数変数のみ。 | +| DP 配列 `dp[0..n]` 版 | O(n²) | O(n) | `dp[i] = Σ dp[left] * dp[right]` | +| 再帰 + メモ化 | O(n²) | O(n) | 再帰呼び出し分のオーバーヘッドあり。 | + +問題の制約(`n <= 19`)ではどの方法でも間に合いますが、 +理論上・実装上ともに **漸化式版が最小限・最速クラス** です。 + +--- + +

Python 実装

+ +以下は CPython 3.11+ / LeetCode / Pylance を意識した実装です。 + +- 再帰は使わず、ループ一つだけ。 +- LeetCode 側が入力制約を保証している前提で、`numTrees` 内では防御的なチェックを省略しています。-(業務コードに組み込む場合は、別途バリデーションラッパーを用意すると良いです。) + +```python +from __future__ import annotations + + +class Solution: + """ + LeetCode 96: Unique Binary Search Trees + + 1..n の連続整数をキーにもつ BST のうち、 + 構造的に異なるものの個数を返す。 + + 数学的には n 番目のカタラン数 C_n を求める問題であり、 + ここでは C_0 = 1, C_n = C_{n-1} * 2 * (2n - 1) / (n + 1) + の漸化式を用いて O(n) / O(1) で計算する。 + """ + + def numTrees(self, n: int) -> int: + """ + 与えられたノード数 n について、構造的に異なる BST の個数を返す。 + + Args: + n: ノード数(LeetCode の制約では 1 <= n <= 19) + + Returns: + 構造的に異なる BST の個数(カタラン数 C_n) + + Note: + LeetCode では入力制約が保証されているため、 + 本メソッド内では追加の入力検証は行っていない。 + """ + # カタラン数 C_0 を初期値とする + c: int = 1 + + # 漸化式: + # C_n = C_{n-1} * 2 * (2n - 1) / (n + 1) + # を利用して C_1..C_n を順に計算する + for i in range(1, n + 1): + # 整数演算で安全に計算できる(C_n は常に整数) + c = c * 2 * (2 * i - 1) // (i + 1) + + return c +``` + +- pylance 的にも型エラーは出ません(戻り値は常に `int`)。 +- 問題制約があるため、`n` が負・巨大・非整数であるケースは LeetCode では発生しません。 + +--- + +

CPython最適化ポイント

+ +この問題規模では「最適化しないと間に合わない」ことはありませんが、CPython の性質を踏まえた書き方になっています。 + +1. **再帰を使わずループにした** + - CPython は関数呼び出しが相対的に重い。 + - 再帰 + メモ化より、`for` ループで漸化式を回す方が軽量。 + +2. **ローカル変数の活用** + - `c` と `i` はメソッド内ローカル。 + - CPython のローカル変数アクセスは高速(`LOAD_FAST`)。 + +3. **標準ライブラリをあえて使わない** + - `math.comb` から `C_n = comb(2n, n) // (n+1)` とすることも可能だが、 + 呼び出しオーバーヘッドや内部実装を考えると、 + 小さな `n` では自前ループと大差ないか、むしろ遅くなりうる。 + - 今回は **ループ1本 + 整数演算** に絞ることでオーバーヘッドを最小化。 + +4. **追加データ構造を作らない** + - `list` や `dict` を使わず、整数だけを更新。 + - ヒープ・GC に負荷をかけないため、メモリ・速度ともに安定。 + +5. **任意精度整数の安全性** + - `n <= 19` の範囲では `C_19 = 1767263190` と 32bit 範囲内。 + - とはいえ Python の `int` は任意精度なので、 + 上限が増えてもオーバーフローの心配はない(計算コストは増える)。 + +--- + +

エッジケースと検証観点

+ +本問題で意識すべき主なケースは以下です(LeetCode 入力では制約違反は来ませんが、思考上の確認として)。 + +1. **最小値 n = 1** + - 入力: `1` + - 期待値: `1` + - 理由: ノード 1 つの BST は 1 通りだけ。 + +2. **小さい値 n = 2, 3** + - `n = 2` → 2 通り + - `n = 3` → 5 通り + - 手書きで全 BST を列挙しやすいので、実装の検証に使いやすい。 + +3. **上限値 n = 19** + - 入力: `19` + - 期待値: `1767263190` + - 計算結果が 32bit 符号付き整数の範囲に収まることも確認できる。 + +4. **理論上の n = 0(LC では出ないが)** + - `C_0 = 1` と定義される。 + - 実装を一般化するなら、`n = 0` を受け取ったとき `1` を返す設計もありうる。 + +これらの値を用いて、以下を確認すると安心です。 + +- 小さな `n` について手計算・列挙と一致するか。 +- `n` を大きくしても(制約範囲内で)エラーやオーバーフローが起きないか。 +- 実装のループが `n` 回ちょうど回っているか。 + +--- + +

FAQ

+ +**Q1. なぜ DP 配列を使わずにカタラン数の漸化式を直接使ってよいのですか?** +A1. BST の通り数 `G(n)` はカタラン数 `C_n` と 1 対 1 で対応することが知られており、 +`G(n)` の再帰関係と `C_n` の再帰関係が一致します。 +そのため、カタラン数の性質(閉形式や漸化式)をそのまま利用して構いません。 + +--- + +**Q2. `C_n = C_{n-1} * 2 * (2n - 1) / (n + 1)` で本当に整数になりますか?** +A2. なります。 +カタラン数は `C_n = (1 / (n + 1)) * binom(2n, n)` と書けるため、 +`(n + 1)` は必ず `binom(2n, n)` を割り切ります。 +実装では整数除算 `//` を使うことで、誤差なく整数が得られます。 + +--- + +**Q3. もっと素直な DP 実装ではダメですか?** +A3. もちろん DP でも正解ですし、`n <= 19` なので十分高速です。 +ただし DP では `O(n²)` の計算と `O(n)` の配列が必要になります。 +カタラン数の漸化式を使えば、より簡潔かつメモリ効率の良い実装になります。 + +--- + +**Q4. どうして木構造(TreeNode)を定義しないのですか?** +A4. この問題が求めているのは「構造的に異なる BST の**個数**」だけで、 +実際の木を構築したり列挙したりする必要はありません。 +そのため、数え上げのための数学的性質(カタラン数)だけを利用するのが最もシンプルです。 + +--- + +**Q5. n の制約がもっと大きくなったらどうなりますか?** +A5. + +- カタラン数は非常に速く増加するため、`C_n` 自体が巨大になります。 +- Python の任意精度整数なら理論上は計算できますが、桁数が増えるにつれて計算コストも増えます。 +- 実用的には、問題設定として `n` に適切な上限が課されることが多いです(この問題では 19)。 + +カタラン数は「**きれいにネストした構造の数**」を数えるときに、めちゃくちゃ頻繁に出てくる数列です。 +図(木・括弧・格子経路)を使って、何が起きているかを順に見ていきます。 + +--- + +## 1. カタラン数って何者? + +**カタラン数列の最初の値:** + +[ +C_0 = 1,\ C_1 = 1,\ C_2 = 2,\ C_3 = 5,\ C_4 = 14,\ C_5 = 42,\ \dots +] + +ざっくりいうと: + +> 「何かを順番に並べたり分割したりするとき、**入れ子構造が崩れないようにする**パターン数」 + +を数えてくれる数です。 + +代表的な「数える対象」は: + +- 正しい括弧列(`()`, `()()`, `(())` など) +- ノード数 `n` の二分探索木(BST)の形の数 +- 正方形を対角線で三角形に分割する方法(多角形の三角形分割) +- 上に行き過ぎない格子経路(Dyck path) + +以下、それぞれ図を使って見ていきます。 + +--- + +## 2. 正しい括弧列とカタラン数 + +### 2.1 n 組の括弧に対して + +- `n` 組の `()` を並べて +- 「**正しくネストした括弧列**」の数 + +がカタラン数 `C_n` になります。 + +### 2.2 具体例:n=1,2,3 + +#### n = 1(1 組) + +- `()` だけ → 1 通り → `C_1 = 1` + +#### n = 2(2 組) + +並べられる括弧列は色々ありますが、「正しい」ものは次の 2 つだけ: + +1. `(())` +2. `()()` + +→ `C_2 = 2` + +簡単な図で表すと: + +```text +(()) : 1組目の中に2組目が入れ子 +()() : 1組目と2組目が並列 +``` + +#### n = 3(3 組)→ C₃ = 5 + +**正しい括弧列**は次の 5 個: + +1. `((()))` +2. `(()())` +3. `(())()` +4. `()(())` +5. `()()()` + +図っぽく並べると: + +```text +1: ((())) +2: (()()) +3: (())() +4: ()(()) +5: ()()() +``` + +この「正しい括弧列の総数」がカタラン数 `C_n` です。 + +--- + +## 3. BST とカタラン数(あなたの問題) + +LeetCode「Unique Binary Search Trees」でやった内容に直結します。 + +### 3.1 n 個のノードを持つ BST の形の数 + +`1..n` の値をそれぞれ1回ずつ使って BST を作るとき: + +- **構造だけ見た BST の個数** = カタラン数 `C_n` + +### 3.2 n=3 の例 + +`1, 2, 3` を使って作れる BST の形は 5 通り(`C_3 = 5`)。 +ざっくり ASCII で表すと: + +1. 根 1、右に 2、その右に 3 + + ```text + 1 + \ + 2 + \ + 3 + ``` + +2. 根 1、右が 3、その左が 2 + + ```text + 1 + \ + 3 + / + 2 + ``` + +3. 根 2、左に 1、右に 3 + + ```text + 2 + / \ + 1 3 + ``` + +4. 根 3、左が 2、その左が 1 + + ```text + 3 + / + 2 + / + 1 + ``` + +5. 根 3、左に 1、その右に 2 + + ```text + 3 + / + 1 + \ + 2 + ``` + +これがまさに `C_3 = 5` です。 + +--- + +## 4. カタラン数の定義と公式 + +### 4.1 再帰的定義(超重要) + +カタラン数は次の再帰で定義されます: + +[ +C_0 = 1 +] + +[ +C_n = \sum_{i=0}^{n-1} C_i \cdot C_{n-1-i} \quad (n \ge 1) +] + +#### BST での意味 + +- ノード数 `n` の BST を考えるとき: + - 根の左部分木が `i` 個のノード + - 根の右部分木が `n-1-i` 個のノード + +- 左右は独立なので組み合わせは `C_i * C_{n-1-i}` +- これをすべての `i` について足すと `C_n` になる + +という感じで、**「根で分割 → 左と右は同じ問題」**という再帰構造を数えているのがカタラン数です。 + +### 4.2 漸化式(計算しやすい形) + +実装でよく使うのはこっち: + +[ +C_0 = 1 +] + +[ +C_n = C_{n-1} \cdot \frac{2(2n - 1)}{n + 1} \quad (n \ge 1) +] + +これを「前のカタラン数をちょっといじって次を出す式」として使います。 + +Python 実装では: + +```python +c = 1 # C_0 +for i in range(1, n + 1): + c = c * 2 * (2 * i - 1) // (i + 1) # C_i を順に更新 +``` + +### 4.3 閉形式(組合せの形) + +もう一つ有名な形: + +[ +C_n = \frac{1}{n+1} \binom{2n}{n} +] + +- `binom(2n, n)` は「2n 個から n 個を選ぶ組合せ」 +- それを `(n+1)` で割ったものが `C_n` + +--- + +## 5. 他の「カタラン数で数えられるもの」 + +カタラン数のすごいところは、「出てくる場面が多すぎる」ことです。 +代表的なものを図と一緒にいくつか。 + +### 5.1 Dyck path(格子経路) + +**条件付き格子経路**: + +- 右方向(`→`)と上方向(`↑`)のステップだけで +- 左下 `(0,0)` から右上 `(n,n)` まで行く道の数 +- ただし、**対角線 `y = x` より上に出てはいけない** + +これの通り数が `C_n` になります。 + +例えば `n=2` のとき: + +- `(0,0)` から `(2,2)` まで、`→→↑↑` の4ステップ +- 条件を満たす経路は 2 個(`C_2 = 2`) + +ざっくり図: + +```text +.↑ ↑ +.→→ . +``` + +上に行き過ぎない経路を数えると、カタラン数になります。 + +--- + +### 5.2 多角形の三角形分割 + +**正多角形を対角線で三角形に分ける方法**も、カタラン数です。 + +- 頂点が `n+2` 個の多角形(例:4角形 = n=2) +- 対角線を引いて「三角形だけ」になるように分割する方法の数が `C_n` + +例:`n = 3` → 5角形 → 三角形分割の数は `C_3 = 5` + +ASCIIでイメージだけ: + +```text +五角形を3つの三角形に分割する線の引き方 → 5通り +``` + +--- + +## 6. なぜこんなにいろんな場面に出るのか? + +共通しているのは「**再帰的な入れ子構造を数えている**」という点です。 + +どの例も、 + +- 何かを分割(根で分ける / 真ん中で分ける / 左右に分ける) +- 左側と右側が、それぞれ「同じルールで再帰的に構成される」 + +という性質を持っています。 + +このとき、 + +- 「左側の構成パターン」 × 「右側の構成パターン」の積を足し上げる +- それが再帰式 `C_n = Σ C_i C_{n-1-i}` の形になる + +→ だから **いろんな「木」や「括弧」や「経路」がカタラン数を生む**、というわけです。 + +--- + +## 7. まとめ + +- カタラン数は、**「きれいにネストした構造」の数を数えるときの王様**みたいな存在。 +- 正しい括弧列、BST の形、多角形の三角形分割、条件付き格子経路…など、登場シーンが非常に多い。 +- 定義・公式: + - 再帰: `C_0 = 1`, `C_n = Σ C_i C_{n-1-i}` + - 漸化式: `C_n = C_{n-1} * 2 * (2n - 1) / (n + 1)` + - 閉形式: `C_n = (1/(n+1)) * binom(2n, n)` + +- コーディングでは、**漸化式でループしながら計算する**のが手軽で高速。 diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_js.ipynb b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_js.ipynb new file mode 100644 index 00000000..ea8b4442 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_js.ipynb @@ -0,0 +1,325 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6a230f43", + "metadata": {}, + "source": [ + "1. 問題の分析\n", + "\n", + "* 実行環境メモ\n", + "\n", + " * Language/Runtime: JavaScript (Node.js v22.14.0)\n", + " * Module: CommonJS\n", + " * 外部ライブラリ: 不使用(Node 標準も未使用)\n", + "\n", + "---\n", + "\n", + "### 競技プログラミング視点での分析\n", + "\n", + "* 問題: `1..n` の連続した値をちょうど一度ずつ使って構成できる「構造的に異なる BST」の個数を求める。\n", + "* 制約: `1 <= n <= 19` とかなり小さい。\n", + "* 典型解: **カタラン数 (Catalan number)**。\n", + "\n", + " * `dp[n] = Σ dp[left] * dp[right]`\n", + " * ここで `left + right = n - 1`(根を 1 つ使うため)。\n", + "* 時間計算量目標: `O(n^2)` で十分間に合う。\n", + "* ナイーブ再帰は指数時間で不必要。閉形式(組合せ)もあるが、`n=19` なら DP の方が実装も安全で読みやすい。\n", + "\n", + "---\n", + "\n", + "### 業務開発視点での分析\n", + "\n", + "* ロジック自体はビジネスロジックとしてもシンプルで、**ボトムアップ DP** で実装するのが最も読みやすくバグも出にくい。\n", + "* 関数は純粋関数(Pure)として実装し、副作用を持たせない。\n", + "* 入力検証:\n", + "\n", + " * 型チェック: `typeof n === 'number'` かつ `Number.isFinite(n)`\n", + " * 整数チェック: `Number.isInteger(n)`\n", + " * 範囲チェック: `1 <= n <= 19` 以外は `RangeError`\n", + "* 例外ポリシー:\n", + "\n", + " * 型がおかしい場合 → `TypeError`\n", + " * 範囲外の場合 → `RangeError`\n", + "* メンテナンス性:\n", + "\n", + " * 変数名は `dp`, `nodes`, `left`, `right` のように意味が分かる名前。\n", + " * カタラン数であることをコメントに残しておくと、あとで読む人も理解しやすい。\n", + "\n", + "---\n", + "\n", + "### JavaScript 特有の考慮点\n", + "\n", + "* **V8 最適化**\n", + "\n", + " * `Number` の単型配列を固定長で `new Array(n + 1)` 生成し、インデックスアクセスのみ実行。\n", + " * ループは `for` を使用 (`for (let i = 0; i <= n; i++)`)。\n", + " * `dp` に数値以外を入れないことで hidden class / element kind を安定させる。\n", + "* **GC 対策**\n", + "\n", + " * 1 回の呼び出しで生成するのは `dp` 配列 1 個だけ。\n", + " * ループ内では新しい配列やオブジェクトを生成しない。\n", + "* **配列操作**\n", + "\n", + " * `push` すら使わず、インデックス代入のみ (`dp[i] = value`)。\n", + " * `shift/unshift` や `sort` などコストの高い操作は不要。\n", + "\n", + "---\n", + "\n", + "2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | JS実装コスト | 可読性 | 備考 |\n", + "| -------------------- | ----- | ----- | ------- | --- | ------------------------------------------- |\n", + "| 方法A: ボトムアップDP(カタラン数) | O(n²) | O(n) | 低 | 高 | 2重ループで `dp[i] = Σ dp[left] * dp[right]` を計算 |\n", + "| 方法B: 組合せの閉形式(カタラン数) | O(n) | O(1) | 中 | 中 | `C_n = (1/(n+1)) * binom(2n, n)` を逐次計算 |\n", + "| 方法C: 再帰 + メモ化 | O(n²) | O(n) | 中 | 中 | 概念的には分かりやすいがコールスタックが不要に深くなる |\n", + "\n", + "---\n", + "\n", + "3. 選択したアルゴリズムと理由\n", + "\n", + "* **選択したアプローチ: 方法A – ボトムアップ DP(カタラン数)**\n", + "* **理由**\n", + "\n", + " * 制約が小さい (`n <= 19`) ため `O(n²)` で十分高速。\n", + " * `dp[i]` の定義が明確で、誰が見ても理解しやすい。\n", + " * 配列 1 本だけで完結し、空間効率も良い。\n", + "* **JavaScript 特有の最適化ポイント**\n", + "\n", + " * `dp` を `new Array(n + 1)` で確保し、`for` ループで添え字アクセスのみにする。\n", + " * ホットパスに入る前に入力検証を済ませることで、ループ中に例外が投げられないようにする。\n", + " * `Number` のみの単型配列を維持して V8 の最適化を邪魔しない。\n", + "\n", + "---\n", + "\n", + "4. コード実装(solution.js)\n", + "\n", + "* モジュール形式: **CommonJS**\n", + "* LeetCode 想定シグネチャ: `var numTrees = function(n) { ... }`\n", + "\n", + "```javascript\n", + "'use strict';\n", + "\n", + "/**\n", + " * LeetCode: Unique Binary Search Trees\n", + " *\n", + " * n 個の異なる値 (1..n) から作ることのできる\n", + " * 「構造的に異なる」二分探索木 (BST) の個数を返す。\n", + " *\n", + " * 本質的には n 番目のカタラン数を求める問題。\n", + " *\n", + " * @param {number} n - ノード数 (1 <= n <= 19)\n", + " * @returns {number} 構造的に異なる BST の個数\n", + " *\n", + " * @throws {TypeError} n が number でない / 有限でない / 整数でない場合\n", + " * @throws {RangeError} n が 1〜19 の範囲外の場合\n", + " *\n", + " * @description\n", + " * 時間計算量: O(n^2)\n", + " * 空間計算量: O(n)\n", + " */\n", + "var numTrees = function (n) {\n", + " // ---- 入力検証(ホットパス前に早期チェック) ----\n", + " if (typeof n !== 'number' || !Number.isFinite(n)) {\n", + " throw new TypeError('n must be a finite number');\n", + " }\n", + " if (!Number.isInteger(n)) {\n", + " throw new TypeError('n must be an integer');\n", + " }\n", + " if (n < 1 || n > 19) {\n", + " throw new RangeError('n must be between 1 and 19');\n", + " }\n", + "\n", + " // ---- DP 配列初期化 ----\n", + " // dp[i] = i 個のノードから作れる BST の個数\n", + " const dp = new Array(n + 1);\n", + "\n", + " // 空木 (0 個) の場合は 1 通り(「何もない」1パターン)\n", + " dp[0] = 1;\n", + "\n", + " // 1〜n 個のノード数について順に計算\n", + " for (let nodes = 1; nodes <= n; nodes++) {\n", + " let count = 0;\n", + "\n", + " // 根に置くノードを 1..nodes として全探索\n", + " // 左部分木に (root - 1) 個、右部分木に (nodes - root) 個のノード\n", + " for (let root = 1; root <= nodes; root++) {\n", + " const leftCount = dp[root - 1];\n", + " const rightCount = dp[nodes - root];\n", + " count += leftCount * rightCount;\n", + " }\n", + "\n", + " dp[nodes] = count;\n", + " }\n", + "\n", + " return dp[n];\n", + "};\n", + "\n", + "// CommonJS 環境(ローカル検証 / CI 用)\n", + "// LeetCode 上では無視される想定\n", + "if (typeof module !== 'undefined' && module.exports) {\n", + " module.exports = { numTrees };\n", + "}\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 54.74 MB\n", + "Beats 19.31%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "5. 追加メモ(JS最適化チェックリスト)\n", + "\n", + "* ループはすべて `for` を使用し、`forEach` / `map` などの高階関数は不使用。\n", + "* 一時オブジェクト・配列の生成は `dp` 1 本のみ。\n", + "* `dp` は数値のみを格納する単型配列で、hidden class / element kind を安定化。\n", + "* 例外はループに入る前の入力検証だけで投げる設計。\n", + "* `n <= 19` なので `Number` でオーバーフローも誤差も問題なし(最大値 `dp[19] = 1767263190`)。\n", + "\n", + "結論から言うと、\n", + "\n", + "* **アルゴリズム的に 0ms / O(1) 追加メモリまで既にほぼ最適**\n", + "* **LeetCode の “54MB / 19%” はほぼ環境オーバーヘッドで、コードではほとんどどうしようもない**\n", + "\n", + "…なんですが、「理論上のメモリ効率」を上げる余地は少しだけあります。\n", + "\n", + "---\n", + "\n", + "## 1. なぜ Memory が悪く見えるのか?\n", + "\n", + "* `n <= 19` なので、`dp` 配列は **長さ 20 の Number 配列** だけです。\n", + "* この配列自体はせいぜい数百バイト〜数 KB レベル。\n", + "* LeetCode の Memory 計測は、\n", + "\n", + " * JS エンジンのオーバーヘッド\n", + " * ランタイム / テストハーネス\n", + " * などを含んだ「プロセス単位の使用メモリ」に近く、\n", + " **あなたのコードの数十バイト〜数 KB の差はほとんどノイズ扱い** になります。\n", + "\n", + "つまり、DPで O(n) の配列を 1 個持ったからといって、メモリ使用量が 54MB → 30MB みたいに劇的に下がることはありません。\n", + "\n", + "---\n", + "\n", + "## 2. 「理論上」もう少し削るなら:O(1) メモリ(カタラン閉形式)\n", + "\n", + "DP を使わず、**カタラン数の漸化式**でそのまま計算すれば、\n", + "「追加メモリは定数(変数数個だけ)」になります。\n", + "\n", + "有名な式:\n", + "\n", + "> C₀ = 1\n", + "> Cₙ = Cₙ₋₁ × 2(2n − 1) / (n + 1)\n", + "\n", + "これを使うと、**配列 `dp` が不要**になります。\n", + "\n", + "### 改善版(理論上 O(1) メモリ)の実装例\n", + "\n", + "LeetCode 形式 + これまでのポリシー(Pure / JSDoc / 例外)を維持した形です。\n", + "\n", + "```javascript\n", + "'use strict';\n", + "\n", + "/**\n", + " * LeetCode: Unique Binary Search Trees\n", + " *\n", + " * n 個の異なる値 (1..n) から作ることのできる\n", + " * 「構造的に異なる」二分探索木 (BST) の個数を返す。\n", + " *\n", + " * 本質的には n 番目のカタラン数 C_n を求める問題。\n", + " * ここでは C_n = C_{n-1} * 2(2n-1)/(n+1) の漸化式を利用し、\n", + " * 追加メモリ O(1) で計算する。\n", + " *\n", + " * @param {number} n - ノード数 (1 <= n <= 19)\n", + " * @returns {number} 構造的に異なる BST の個数\n", + " *\n", + " * @throws {TypeError} n が number でない / 有限でない / 整数でない場合\n", + " * @throws {RangeError} n が 1〜19 の範囲外の場合\n", + " *\n", + " * @description\n", + " * 時間計算量: O(n)\n", + " * 空間計算量: O(1)\n", + " */\n", + "var numTrees = function (n) {\n", + " // ---- 入力検証(ホットパス前に早期チェック)----\n", + " if (typeof n !== 'number' || !Number.isFinite(n)) {\n", + " throw new TypeError('n must be a finite number');\n", + " }\n", + " if (!Number.isInteger(n)) {\n", + " throw new TypeError('n must be an integer');\n", + " }\n", + " if (n < 1 || n > 19) {\n", + " throw new RangeError('n must be between 1 and 19');\n", + " }\n", + "\n", + " // ---- カタラン数 C_n を漸化式で計算 (C_0 = 1) ----\n", + " // C_n = C_{n-1} * 2(2n - 1) / (n + 1)\n", + " let c = 1; // C_0\n", + "\n", + " for (let i = 1; i <= n; i++) {\n", + " c = (c * 2 * (2 * i - 1)) / (i + 1);\n", + " }\n", + "\n", + " // n <= 19 の範囲では C_n は 2^53 未満 なので\n", + " // IEEE754 の倍精度浮動小数で誤差なく表現できる。\n", + " // 一応安全のため切り捨て/丸めしてもよい(コストはほぼ無視できる)。\n", + " return c;\n", + "};\n", + "\n", + "// CommonJS 環境(ローカル検証 / CI 用)\n", + "if (typeof module !== 'undefined' && module.exports) {\n", + " module.exports = { numTrees };\n", + "}\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 53.24 MB\n", + "Beats 88.03%\n", + "\n", + "```\n", + "\n", + "### このアプローチのポイント\n", + "\n", + "* **時間計算量**: `O(n²)` → `O(n)`(とはいえ n<=19 なので体感差はゼロ)\n", + "* **空間計算量**: `O(n)` → `O(1)`(理論上)\n", + "* JS 的にも:\n", + "\n", + " * 配列がなく、単に `Number` 変数 `c` を更新するだけ\n", + " * hidden class/element kind の心配も不要\n", + " * 生成オブジェクト数がさらに減る\n", + "\n", + "LeetCode のメモリランキングが劇的に上がる保証はありませんが、\n", + "**アルゴリズム理論上は最もメモリ効率の良い形**になっています。\n", + "\n", + "---\n", + "\n", + "## 3. 実務・競技的なまとめ\n", + "\n", + "* すでに Runtime 0ms (100%) なので、速度面は「これ以上やることはない」です。\n", + "* メモリは計測の特性上、「環境オーバーヘッド」が支配的で、\n", + " コード変更で大きく上下させるのはほぼ不可能です。\n", + "* それでもこだわるなら:\n", + "\n", + " * DP 配列をやめてカタラン数の漸化式で O(1) メモリにする(上の実装)。\n", + " * LeetCode 用に限れば、入力バリデーションを削ると(ほんの少しだけ)クリーンなコード&わずかなメモリ削減になるかもしれません。\n", + "\n", + "なので、\n", + "\n", + "* **アルゴリズム・実装品質としてはすでに十分トップレベル**\n", + "* 趣味・学習として「理論上も最適」を目指すなら、上のカタラン漸化式版に差し替え\n", + "\n", + "くらいの認識でOKです。" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_python.ipynb b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_python.ipynb new file mode 100644 index 00000000..d9bd43fe --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_python.ipynb @@ -0,0 +1,438 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f699a27c", + "metadata": {}, + "source": [ + "### 1. 問題分析結果\n", + "\n", + "対象: LeetCode「Unique Binary Search Trees」\n", + "`1..n` の連続整数をすべて使って構成できる「構造的に異なる BST」の数を求める問題です。\n", + "\n", + "---\n", + "\n", + "#### 競技プログラミング視点\n", + "\n", + "* **制約**: `1 <= n <= 19`\n", + "* **数学的背景**:\n", + "\n", + " * 「n 番目のカタラン数 Cₙ」を求める問題\n", + " * 代表的な定義\n", + "\n", + " * DP 定義: `Cₙ = Σ Cᵢ * Cₙ₋₁₋ᵢ (i=0..n-1)`\n", + " * 漸化式: `C₀ = 1`, `Cₙ = Cₙ₋₁ * 2(2n − 1) / (n + 1)`\n", + "* **必要計算量**\n", + "\n", + " * `n <= 19` なので、`O(n²)` でも余裕で間に合う\n", + " * ただし、理論上は `O(n)` / `O(1)` の漸化式が最も軽い\n", + "* **メモリ方針**\n", + "\n", + " * 配列 DP なら `O(n)` の extra memory\n", + " * 漸化式なら変数数個で `O(1)` にできる\n", + " * どちらも CPython では誤差レベルだが、理論上は `O(1)` が優位\n", + "\n", + "---\n", + "\n", + "#### 業務開発視点\n", + "\n", + "* **可読性・保守性**\n", + "\n", + " * 「カタラン数問題である」ことをコメントと docstring で明示\n", + " * 数式とコードが対応するように、ループや変数名をシンプルに\n", + "* **型安全性**\n", + "\n", + " * LeetCode シグネチャは `def numTrees(self, n: int) -> int:`\n", + " * 型ヒントにより Pylance/型チェッカがサポート\n", + "* **エラーハンドリング**\n", + "\n", + " * 業務コードなら:\n", + "\n", + " * `n` の型・有限性・整数性・範囲を検証\n", + " * 想定外の入力は `TypeError` / `ValueError` を送出\n", + " * 競プロ実装では省略して速度最優先\n", + "\n", + "---\n", + "\n", + "#### Python特有分析\n", + "\n", + "* **データ構造選択**\n", + "\n", + " * 計算はスカラー `int` のみで足りるので、`list/deque/set/dict` は不要\n", + "* **標準ライブラリ**\n", + "\n", + " * 特に必要ない(`math.comb` などで組合せから計算することもできるが、\n", + " 小さい `n` では自前ループの方が分かりやすい)\n", + "* **CPython最適化**\n", + "\n", + " * Python の `int` は任意精度だが、`n<=19` では桁数は非常に小さい\n", + " * 単純な `for` ループ + ローカル変数更新は CPython の JIT なしでも十分高速\n", + " * 再帰を避けてループにすることで、コールスタック・関数呼び出しオーバーヘッドを削減\n", + "\n", + "---\n", + "\n", + "### 2. アルゴリズム比較表\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 |\n", + "| -------------- | --------- | -------- | ----------- | --- | --------------------- | ---------- | ------------------------------ |\n", + "| 方法A: カタラン数の漸化式 | **O(n)** | **O(1)** | 低 | ★★★ | 不要 | 適 | ループ1本で Cₙ を計算 |\n", + "| 方法B: DP(定義どおり) | **O(n²)** | **O(n)** | 低〜中 | ★★★ | 不要 | 適 | `dp[i] = Σ dp[left]*dp[right]` |\n", + "| 方法C: 再帰 + メモ化 | **O(n²)** | **O(n)** | 中 | ★★☆ | `functools.lru_cache` | やや不適 | 再帰のオーバーヘッドが増える |\n", + "\n", + "* 今回は **方法A: カタラン数の漸化式** を採用します。\n", + "\n", + "---\n", + "\n", + "### 3. 採用アルゴリズムと根拠\n", + "\n", + "* **選択したアプローチ**:\n", + " カタラン数の漸化式\n", + " `C₀ = 1`, `Cₙ = Cₙ₋₁ * 2(2n − 1) / (n + 1)` を用いた **O(n)/O(1)** アルゴリズム\n", + "\n", + "* **選択理由**\n", + "\n", + " * 計算量:\n", + "\n", + " * `O(n²)` DP より理論上軽い `O(n)` で十分に速い\n", + " * `n<=19` なので差は小さいが、コードも簡潔なのでデメリット無し\n", + " * Python特性:\n", + "\n", + " * 任意精度整数によりオーバーフローを気にしなくてよい\n", + " * 整数演算のみ・ループ 1 本で、CPython が扱いやすい形\n", + " * 保守性・可読性:\n", + "\n", + " * 数式→コードの対応が直感的\n", + " * DP 配列などを持たないので読みやすく、バグの入り込み余地が少ない\n", + "\n", + "* **Python最適化戦略**\n", + "\n", + " * `for i in range(1, n + 1):` の単純な整数ループ\n", + " * `c` をローカル変数に閉じ込めて更新(ローカル変数アクセスは CPython で高速)\n", + " * 標準ライブラリやオブジェクト生成を極力避ける(今回は全く不要)\n", + "\n", + "* **トレードオフ**\n", + "\n", + " * DP 版は「左右部分木の数の掛け算」という構造が直感的に分かりやすいが、\n", + " 漸化式版も数学的には標準的でコメントを添えれば十分理解しやすい。\n", + " * 今回は「競プロ向け速度+業務向けの簡潔さ」のバランスが良い漸化式版を選択。\n", + "\n", + "---\n", + "\n", + "### 4. 実装パターン\n", + "\n", + "* **業務開発版 `_numTrees_production`**\n", + "\n", + " * 型チェック・値チェックを行い、異常値は例外で通知\n", + " * ドキュメントコメント(docstring)付き\n", + "* **競技プログラミング版 `_numTrees_competitive`**\n", + "\n", + " * 入力が制約を満たす前提で、チェックを一切省略\n", + " * ループのみの最小限実装\n", + "* LeetCode が呼び出すのは `numTrees` メソッドなので、\n", + " **競技用実装を呼ぶ**形にします。\n", + "\n", + "---\n", + "\n", + "### 5. 実装コード(LeetCode形式 / CPython 3.11 / Pylance対応)\n", + "\n", + "```python\n", + "from __future__ import annotations\n", + "\n", + "from typing import Final\n", + "\n", + "\n", + "class Solution:\n", + " \"\"\"\n", + " Unique Binary Search Trees 問題を解くクラス。\n", + "\n", + " - numTrees: LeetCode が呼び出すエントリポイント(競技プログラミング向け実装を利用)\n", + " - _numTrees_production: 業務開発を意識した防御的実装(入力検証付き)\n", + " - _numTrees_competitive: 競技プログラミング向けの高速・簡潔実装\n", + " \"\"\"\n", + "\n", + " # 制約を定数として明示(業務コードを意識した書き方)\n", + " _MIN_N: Final[int] = 1\n", + " _MAX_N: Final[int] = 19\n", + "\n", + " def numTrees(self, n: int) -> int:\n", + " \"\"\"\n", + " LeetCode 用エントリポイント。\n", + "\n", + " 競技プログラミング向けの高速実装を利用する。\n", + " LeetCode 側が制約を保証している前提なので、ここでは入力検証を行わない。\n", + "\n", + " Args:\n", + " n: ノード数 (1 <= n <= 19)\n", + "\n", + " Returns:\n", + " 構造的に異なる BST の個数\n", + " \"\"\"\n", + " return self._numTrees_competitive(n)\n", + "\n", + " # ==== 業務開発向け実装 ==================================================\n", + "\n", + " def _numTrees_production(self, n: int) -> int:\n", + " \"\"\"\n", + " 業務開発向けの防御的実装。\n", + "\n", + " 入力の型チェック・値チェックを行い、\n", + " 異常値の場合は TypeError / ValueError を送出する。\n", + "\n", + " Args:\n", + " n: ノード数\n", + "\n", + " Returns:\n", + " 構造的に異なる BST の個数\n", + "\n", + " Raises:\n", + " TypeError: n が int でない、または bool である場合\n", + " ValueError: n が有限整数でない、あるいは許容範囲外の場合\n", + "\n", + " Complexity:\n", + " Time: O(n)\n", + " Space: O(1)\n", + " \"\"\"\n", + " # --- 入力検証 ---\n", + " if not isinstance(n, int) or isinstance(n, bool):\n", + " # bool は int のサブクラスなので明示的に除外\n", + " raise TypeError(\"n must be an integer\")\n", + "\n", + " # Python の int は任意精度だが、NaN/inf は float なのでここには来ない\n", + "\n", + " if n < self._MIN_N or n > self._MAX_N:\n", + " raise ValueError(\n", + " f\"n must be between {self._MIN_N} and {self._MAX_N}, got {n}\"\n", + " )\n", + "\n", + " # --- メインロジック(カタラン数の漸化式) ---\n", + " # C_0 = 1\n", + " c: int = 1\n", + "\n", + " # C_n = C_{n-1} * 2(2n - 1) / (n + 1)\n", + " # i は「今計算している n の値」\n", + " for i in range(1, n + 1):\n", + " # 整数演算のみで誤差は発生しない\n", + " c = c * 2 * (2 * i - 1) // (i + 1)\n", + "\n", + " return c\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 17.61 MB\n", + "Beats 79.52%\n", + "\n", + " # ==== 競技プログラミング向け実装 ========================================\n", + "\n", + " def _numTrees_competitive(self, n: int) -> int:\n", + " \"\"\"\n", + " 競技プログラミング向けの最適化実装。\n", + "\n", + " - LeetCode が制約を満たす入力のみを投げてくる前提で、\n", + " 型チェックや範囲チェックを行わない。\n", + " - カタラン数の漸化式で O(n) / O(1) で計算する。\n", + "\n", + " Args:\n", + " n: ノード数(1 <= n <= 19 が保証されている前提)\n", + "\n", + " Returns:\n", + " 構造的に異なる BST の個数\n", + "\n", + " Complexity:\n", + " Time: O(n)\n", + " Space: O(1)\n", + " \"\"\"\n", + " c: int = 1\n", + " for i in range(1, n + 1):\n", + " c = c * 2 * (2 * i - 1) // (i + 1)\n", + " return c\n", + "\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 17.74 MB\n", + "Beats 50.04%\n", + "\n", + "```\n", + "\n", + "* `from __future__ import annotations` により前方参照型も安全(ここではほぼおまじないですが、3.11 ではこれで型解析が軽くなります)。\n", + "* `Final` を使って制約値を定数として明示。\n", + "* Pylance 的にも、型未解決や `Any` 由来の警告は出ない構成です。\n", + "\n", + "---\n", + "\n", + "### 6. 検証(思考実験レベル)\n", + "\n", + "* **境界値**\n", + "\n", + " * `n = 1` → C₁ = 1\n", + " * `n = 2` → C₂ = 2\n", + " * `n = 3` → C₃ = 5\n", + " * `n = 19` → C₁₉ = 1767263190(問題の上限付近)\n", + "* **業務版でのエラーハンドリング**\n", + "\n", + " * `n = 0` → `ValueError`\n", + " * `n = 20` → `ValueError`\n", + " * `n = 1.5` → `TypeError`\n", + " * `n = True` → `TypeError`(bool 明示除外)\n", + "\n", + "LeetCode 提出時は `numTrees` の中身だけあれば十分ですが、\n", + "このクラス全体を貼っても動作します(LeetCode は `numTrees` だけを呼びます)。\n", + "\n", + "結論から言うと、この状態なら **ほぼ限界まで仕上がっていて、実質これ以上いじる価値はほとんどない** です 🎯\n", + "\n", + "* Runtime: **0 ms / 100%** → これ以上は出ません\n", + "* Memory: **17.61MB / 79.52%** & **17.74MB / 50.04%** → どちらも十分優秀で、差はほぼ「計測ノイズ」\n", + "\n", + "とはいえ、「理論と趣味の世界でどこまで削れるか?」という観点でコメントします。\n", + "\n", + "---\n", + "\n", + "## 1. 今のコードの評価\n", + "\n", + "### アルゴリズム面\n", + "\n", + "* カタラン数の漸化式:\n", + "\n", + " ```python\n", + " C_0 = 1\n", + " C_n = C_{n-1} * 2(2n - 1) / (n + 1)\n", + " ```\n", + "* 計算量:\n", + "\n", + " * 時間: `O(n)`(n<=19 なので 19 回のループ)\n", + " * 空間: `O(1)`(`c` と `i` だけ)\n", + "* Python 的にも:\n", + "\n", + " * 再帰なし\n", + " * DP 配列なし\n", + " * 任意精度整数でも桁数が小さいのでオーバーヘッドも極小\n", + "\n", + "→ アルゴリズムとしては **理論上も実装上も最適クラス** です。\n", + "\n", + "---\n", + "\n", + "## 2. LeetCode のメモリについて\n", + "\n", + "メモリ 17.61MB と 17.74MB の差は:\n", + "\n", + "* Python インタプリタの状態\n", + "* テストケース実行のタイミング\n", + "* ジャッジ環境の他プロセス状況\n", + "\n", + "などに左右されるため、**あなたの数行のコードの差ではありません**。\n", + "\n", + "実際にあなたのコードが使っている追加メモリは、\n", + "\n", + "* `Solution` インスタンス\n", + "* `int` 2つ (`n`, `c`) とループ用 `i`\n", + "\n", + "くらいなので、数十〜数百バイトレベルです。\n", + "つまり、**コード上で工夫しても MB 単位ではほぼ動かない** と思っておいてOKです。\n", + "\n", + "---\n", + "\n", + "## 3. 「それでも詰めたい」場合の候補\n", + "\n", + "### 3.1 LeetCode ガチ競プロモード(実務用メソッドを外す)\n", + "\n", + "今のクラスは「業務版 + 競プロ版」の 2 本立てですが、\n", + "LeetCode 的には **`numTrees` 一つあれば十分** です。\n", + "\n", + "メモリスコアだけをガチで詰めたいなら、\n", + "`_numTrees_production` や定数、docstring を落とした最小構成にする、という選択があります。\n", + "\n", + "```python\n", + "class Solution:\n", + " def numTrees(self, n: int) -> int:\n", + " c = 1\n", + " for i in range(1, n + 1):\n", + " c = c * 2 * (2 * i - 1) // (i + 1)\n", + " return c\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 17.63 MB\n", + "Beats 79.52%\n", + "\n", + "```\n", + "\n", + "* クラス属性 `_MIN_N/_MAX_N` も削除\n", + "* docstring も削除\n", + "* メソッドは 1 本だけ\n", + "\n", + "**ただし**:\n", + "\n", + "* これで 17.61MB → 15MB みたいに激減することはまずありません。\n", + "* 数十 KB〜数百 KB 単位の差が出るかどうか、というレベルです。\n", + "\n", + "### 3.2 O(1) 時間の「定数テーブル」版(趣味 & 面白さ枠)\n", + "\n", + "「ループすらなくす」方向性として、カタラン数を配列にベタ書きする方法もあります。\n", + "\n", + "```python\n", + "class Solution:\n", + " def numTrees(self, n: int) -> int:\n", + " # 1 <= n <= 19 が前提\n", + " table = [\n", + " 0,\n", + " 1,\n", + " 2,\n", + " 5,\n", + " 14,\n", + " 42,\n", + " 132,\n", + " 429,\n", + " 1430,\n", + " 4862,\n", + " 16796,\n", + " 58786,\n", + " 208012,\n", + " 742900,\n", + " 2674440,\n", + " 9694845,\n", + " 35357670,\n", + " 129644790,\n", + " 477638700,\n", + " 1767263190,\n", + " ]\n", + " return table[n]\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 17.63 MB\n", + "Beats 79.52%\n", + "\n", + "```\n", + "\n", + "* 時間計算量は理論上 `O(1)`\n", + "* 空間は `O(1)`(配列 20 要素)と見なせる(n が固定上限 19 のため)\n", + "* 実際のメモリスコアが良くなる保証はなく、むしろ配列ぶんだけ増える可能性もあります😅\n", + " (とはいえ、これも KB 未満レベルの話)\n", + "\n", + "---\n", + "\n", + "## 4. 結論\n", + "\n", + "* **今の実装は完全に実戦レベルで最適クラス** です。\n", + "\n", + " * 速度: 0ms / 100% → これ以上なし\n", + " * メモリ: 17MB 台で 50〜80% → 十分上位\n", + "* ここから先は、\n", + "\n", + " * LeetCode 上位数%を狙うための誤差調整\n", + " * or 「どこまでミニマルに書けるか」という趣味・美学の世界" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_ts.ipynb b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_ts.ipynb new file mode 100644 index 00000000..9aa72bd7 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/96. Unique Binary Search Trees/gpt 5.1 thinking customized/UniqueBinarySearchTrees_ts.ipynb @@ -0,0 +1,358 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "69b4058e", + "metadata": {}, + "source": [ + "### 1. 問題の分析\n", + "\n", + "#### 競技プログラミング視点での分析\n", + "\n", + "* 問題: `1..n` の値をそれぞれ一度ずつ使って構成できる「構造的に異なる BST」の個数を求める。\n", + "* 制約: `1 <= n <= 19` と小さい。\n", + "* 数学的には「**n 番目のカタラン数 Cₙ**」を求める問題。\n", + "\n", + "代表的な解法:\n", + "\n", + "1. **DP(カタラン数 DP 定義)**\n", + "\n", + " * `dp[i] = Σ dp[left] * dp[right]` (`left + right = i - 1`)\n", + " * 時間: `O(n^2)` / 空間: `O(n)`\n", + "2. **カタラン数の漸化式で直接計算**\n", + "\n", + " * `C₀ = 1`\n", + " * `Cₙ = Cₙ₋₁ × 2(2n − 1) / (n + 1)`\n", + " * 時間: `O(n)` / 空間: `O(1)`\n", + "\n", + "`n <= 19` なのでどちらでも余裕で間に合うが、**O(1) 空間の漸化式**が理論的に最もメモリ効率が高い。\n", + "\n", + "#### 業務開発視点での分析\n", + "\n", + "* **型安全性**\n", + "\n", + " * TypeScript のシグネチャを `function numTrees(n: number): number` として固定し、\n", + " 「非数・非整数・範囲外」の入力は **実行時に guard** して `TypeError` / `RangeError` を投げる。\n", + "* **可読性・保守性**\n", + "\n", + " * 「これはカタラン数問題である」というコメントを入れ、\n", + " 数式とループの対応が一目で分かるようにする。\n", + " * ループ変数・変数名を `i`, `c` などシンプルかつ数学的意味に対応させる。\n", + "* **エラーハンドリング**\n", + "\n", + " * `typeof n !== 'number' || !Number.isFinite(n)` → `TypeError`\n", + " * `!Number.isInteger(n)` → `TypeError`\n", + " * `n < 1 || n > 19` → `RangeError`\n", + "\n", + "LeetCode 上では本来ここまでのチェックは不要ですが、**業務前提の「防御的コーディング」**としては良い形です。\n", + "\n", + "#### TypeScript特有の考慮点\n", + "\n", + "* **型推論**\n", + "\n", + " * `n: number` を明示し、それ以外は基本的に型推論に任せる(`let c = 1` など)。\n", + " * 暗黙の `any` は一切出ないコードにする(strict モード対応)。\n", + "* **ジェネリクス**\n", + "\n", + " * この問題は「入力 1 個の数値 → 出力 1 個の数値」なので、\n", + " 無理にジェネリクスを使うと逆に可読性が落ちるケース。\n", + " → **ここではジェネリクスは使わず、単純な数値関数として実装**。\n", + "* **コンパイル時最適化**\n", + "\n", + " * 分岐・ループ構造をシンプルに保ち、型情報が単純な `number` のみになるようにすることで、\n", + " JS エミット後も JIT(V8)が最適化しやすい。\n", + "* **null/undefined 安全性**\n", + "\n", + " * TS シグネチャで `n: number` とすることで、\n", + " 呼び出し元で `null | undefined` を渡すとコンパイル時にエラーにできる。\n", + " * それでも実行時に変な値が来た時のために型ガードを入れておく。\n", + "\n", + "---\n", + "\n", + "### 2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 |\n", + "| ----------------------- | ----- | ----- | ------- | ---- | --- | ------------------------------ |\n", + "| 方法A: カタラン数 漸化式(O(1)メモリ) | O(n) | O(1) | 低 | 高 | 高 | ループ1本で Cₙ を直接計算 |\n", + "| 方法B: DP(カタラン数の定義に従う) | O(n²) | O(n) | 低〜中 | 高 | 高 | `dp[i] = Σ dp[left]*dp[right]` |\n", + "| 方法C: 再帰 + メモ化 | O(n²) | O(n) | 中 | 高 | 中 | コールスタック分のオーバーヘッドあり |\n", + "\n", + "`n <= 19` なのでどれでも十分ですが、**業務+競プロの両視点**からすると:\n", + "\n", + "* 計算量: A が最良 (`O(n)` / `O(1)`)\n", + "* 実装の簡潔さ: A, B はどちらも簡単だが、A の方がループ1本で済む\n", + "* TypeScript的コスト: どれも簡単に型安全に書けるが、A/B ともにほぼ `number` だけ\n", + "\n", + "→ **方法A(カタラン数の漸化式)を採用**します。\n", + "\n", + "---\n", + "\n", + "### 3. 選択したアルゴリズムと理由\n", + "\n", + "* **選択したアプローチ**:\n", + " **方法A: カタラン数の漸化式 Cₙ = Cₙ₋₁ × 2(2n − 1) / (n + 1)** を用いた O(1) 空間解法\n", + "\n", + "* **理由**\n", + "\n", + " * 計算量\n", + "\n", + " * `n <= 19` なので DP でも問題ないが、理論上最も軽い `O(n)` / `O(1)` を選択。\n", + " * TypeScript環境での型安全性\n", + "\n", + " * 計算に関わる値は全て `number` 型のみ。ジェネリクス等も不要で素直に書ける。\n", + " * 入力 `n` に対する型と実行時ガードを明確に分離できる。\n", + " * 保守性・可読性\n", + "\n", + " * 数学的定義とコードが 1:1 に対応しやすく、コメントでの説明も簡単。\n", + " * `for (let i = 1; i <= n; i++)` という単純なループで式を回すだけ。\n", + "\n", + "* **TypeScript特有の最適化ポイント**\n", + "\n", + " * `numTrees(n: number): number` というシンプルなシグネチャで、\n", + " コンパイル時に不正な呼び出しを排除。\n", + " * strict モードを前提に、暗黙の any や `null | undefined` を排除。\n", + " * 実行時ガードを入れておくことで、型情報が外れた場合も安全に失敗させる。\n", + "\n", + "---\n", + "\n", + "### 4. 実装コード(TypeScript / ESM / LeetCodeフォーマット)\n", + "\n", + "* 想定環境\n", + "\n", + " * Node.js v22.14.0\n", + " * ESM 形式 (`export` / `export default` を使用)\n", + " * strict モード(`tsconfig.json` 側で有効と想定)\n", + "* LeetCode での解答形式を優先し、**メイン関数シグネチャは `function numTrees(n: number): number`** とします。\n", + "\n", + "```typescript\n", + "/**\n", + " * Unique Binary Search Trees\n", + " *\n", + " * n 個の異なる値 (1..n) から作ることのできる\n", + " * 「構造的に異なる」二分探索木 (BST) の個数を返す。\n", + " *\n", + " * 本質的には n 番目のカタラン数 Cₙ を求める問題。\n", + " * ここでは Cₙ = Cₙ₋₁ * 2(2n - 1) / (n + 1) の漸化式を利用し、\n", + " * 時間 O(n), 追加空間 O(1) で計算する。\n", + " *\n", + " * @param n - ノード数 (1 <= n <= 19)\n", + " * @returns 構造的に異なる BST の個数\n", + " *\n", + " * @throws {TypeError} n が number でない / 有限でない / 整数でない場合\n", + " * @throws {RangeError} n が 1〜19 の範囲外の場合\n", + " *\n", + " * @complexity Time: O(n), Space: O(1)\n", + " */\n", + "function numTrees(n: number): number {\n", + " // ---- 実行時入力検証(防御的プログラミング)----\n", + "\n", + " if (typeof n !== 'number' || !Number.isFinite(n)) {\n", + " throw new TypeError('n must be a finite number');\n", + " }\n", + "\n", + " if (!Number.isInteger(n)) {\n", + " throw new TypeError('n must be an integer');\n", + " }\n", + "\n", + " if (n < 1 || n > 19) {\n", + " throw new RangeError('n must be between 1 and 19');\n", + " }\n", + "\n", + " // ---- カタラン数 C_n を漸化式で計算 (C_0 = 1) ----\n", + " // C_n = C_{n-1} * 2(2n - 1) / (n + 1)\n", + " // ここでループ変数 i は「現在計算している n」を表す。\n", + " let c = 1; // C_0\n", + "\n", + " for (let i = 1; i <= n; i++) {\n", + " // 計算途中も常に number。n <= 19 の範囲では C_n < 2^31 なので\n", + " // IEEE754 倍精度でも誤差なく表現できる。\n", + " c = (c * 2 * (2 * i - 1)) / (i + 1);\n", + " }\n", + "\n", + " // 理論上誤差は出ないが、念のため整数化したい場合は Math.round(c) も検討可。\n", + " return c;\n", + "}\n", + "\n", + "// ESM としてのエクスポート(ローカル実行/CI 用)\n", + "// LeetCode 上ではこの export 部分は無視される想定\n", + "export { numTrees };\n", + "export default numTrees;\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 53.50 MB\n", + "Beats 97.10%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "### 5. TypeScript固有の最適化観点(この実装にどう効いているか)\n", + "\n", + "* **コンパイル時エラー防止**\n", + "\n", + " * `numTrees(n: number): number` により、呼び出し元で `string` や `null` などを渡すとコンパイル時エラー。\n", + " * strict モード下で暗黙の `any` が発生しないよう、全ての変数に明確な型が付き、推論もシンプル。\n", + "* **readonly / イミュータビリティ**\n", + "\n", + " * 入力 `n` はプリミティブ値で、関数内で再代入しないことで実質的にイミュータブル。\n", + " * 配列などの破壊的操作がないため、副作用の心配はない(Pure function)。\n", + "* **シンプルな型による最適化しやすさ**\n", + "\n", + " * 計算に登場するのは `number` 型のみ。\n", + " * 条件分岐も入力検証部分と本体ロジックがきれいに分かれているため、\n", + " JIT もホットパス(for ループ)を最適化しやすい。\n", + "\n", + "---\n", + "結論から言うと、\n", + "\n", + "> **ほぼ限界まで最適化されていて、LeetCode の指標的には改善の余地はほとんどありません。**\n", + "\n", + "* Runtime: **0ms / 100%** → これ以上は出ません(0ms より速い値はない)\n", + "* Memory: **53.50MB / 97.10%** → 上位数%なので、実質トップクラス\n", + "\n", + "なので、「ガチでまだやれること」はかなり少ないです。そのうえで、**理論上 or コンテスト用にさらに詰めるなら**という観点でお話します。\n", + "\n", + "---\n", + "\n", + "## 1. LeetCode のメモリ指標について\n", + "\n", + "TS で書いていても、**実際に動いているのはコンパイル後の JavaScript** です。\n", + "\n", + "* 測定されている 53MB という数字は、\n", + "\n", + " * JS エンジン自体のオーバーヘッド\n", + " * テストハーネス・ジャッジ環境のオーバーヘッド\n", + " * あなたのコード\n", + "を全部まとめた「プロセスレベル」のメモリに近いものです。\n", + "\n", + "今回のアルゴリズムで実際に使っているメモリは、\n", + "\n", + "* 変数 `n` と `c`\n", + "* ループ内の数個の一時的な `number`\n", + "\n", + "くらいで、**数十バイト〜数百バイトレベル**です。\n", + "つまり、**コード側で削れるメモリは誤差の範囲**で、53.5MB が 20MB とかになることはありません。\n", + "\n", + "---\n", + "\n", + "## 2. それでも「理論上」詰めるとしたら\n", + "\n", + "### (1) LeetCode 提出用に「ガチ競プロモード」にする\n", + "\n", + "今の実装は業務スタイル寄りで、ちゃんと入力検証していますよね:\n", + "\n", + "* `typeof n !== 'number'` チェック\n", + "* `Number.isFinite`\n", + "* `Number.isInteger`\n", + "* 範囲チェックして `RangeError` / `TypeError` を投げる\n", + "\n", + "**LeetCode に限れば、これらは全部削って OK** です(ジャッジは必ず条件を満たした入力しか投げてこない前提)。\n", + "\n", + "#### 競プロ用・最小限版イメージ(TypeScript)\n", + "\n", + "```ts\n", + "function numTrees(n: number): number {\n", + " // C_0 = 1\n", + " let c = 1;\n", + "\n", + " for (let i = 1; i <= n; i++) {\n", + " c = (c * 2 * (2 * i - 1)) / (i + 1);\n", + " }\n", + "\n", + " return c;\n", + "}\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 54.82 MB\n", + "Beats 65.22%\n", + "\n", + "```\n", + "\n", + "* 分岐がなくなり、コードサイズ・命令数が減ります。\n", + "* 実際の 0ms / 53MB という表示はほとんど変わらない可能性が高いですが、\n", + " 「純粋にアルゴリズム部分だけ」に絞り込んだ形になります。\n", + "\n", + "### (2) 完全に配列テーブルで返す O(1) 解法\n", + "\n", + "「ループすら消したい」という趣味レベルの詰め方もあります。\n", + "\n", + "```ts\n", + "function numTrees(n: number): number {\n", + " // n は 1〜19 しか来ない前提(LeetCode の制約に依存)\n", + " const C: readonly number[] = [\n", + " 0,\n", + " 1, // C1\n", + " 2, // C2\n", + " 5, // C3\n", + " 14, // C4\n", + " 42, // C5\n", + " 132, // ...\n", + " 429,\n", + " 1430,\n", + " 4862,\n", + " 16796,\n", + " 58786,\n", + " 208012,\n", + " 742900,\n", + " 2674440,\n", + " 9694845,\n", + " 35357670,\n", + " 129644790,\n", + " 477638700,\n", + " 1767263190, // C19\n", + " ];\n", + " return C[n];\n", + "}\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 55.08 MB\n", + "Beats 57.97%\n", + "\n", + "```\n", + "\n", + "* 時間計算量: 理論上 **O(1)**\n", + "* 空間: 定数個の `number`(配列 1 個)\n", + "* 実用的には、今の O(n) 漸化式と体感差はゼロです。\n", + "\n", + "---\n", + "\n", + "## 3. TypeScript 的な観点\n", + "\n", + "* **TS の型はランタイムでは消える**ので、\n", + "\n", + " * 型注釈を減らしても\n", + " * ジェネリクスを使っても使わなくても\n", + " → **LeetCode のメモリ/速度には基本的に影響しません**。\n", + "* 影響するのは「生成された JS の構造」だけです。\n", + "\n", + " * すでに関数 1 個 + ループというミニマル構成なので、\n", + " * ここからさらに大きく速くしたりメモリを削るのはほぼ不可能です。\n", + "\n", + "---\n", + "\n", + "## 4. まとめ\n", + "\n", + "* **現状の結果(0ms / メモリ 97%)は、ほぼ「これ以上ないレベル」** です。\n", + "* さらに詰めるとすれば:\n", + "\n", + " * LeetCode 向けに入力検証を完全に削る(ガチ競プロ仕様)\n", + " * テーブル埋めの O(1) 解法にする(趣味の世界・理論上)\n", + "* ただし、どちらも実測の 0ms / 53MB という表示が劇的に変わることは期待できません。\n", + " 今の実装は **競技的にも業務的にも十分トップクラス**と言って問題ないです 👍\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "typescript" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}