From 6c698fbdf3970b091eb0b2103c823fc61745eb8a Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sun, 26 Oct 2025 20:32:52 +0900 Subject: [PATCH] =?UTF-8?q?leetcode=2093.=20Restore=20IP=20Addresses=20?= =?UTF-8?q?=E6=B7=B1=E3=81=95=E5=84=AA=E5=85=88=E6=8E=A2=E7=B4=A2=EF=BC=88?= =?UTF-8?q?DFS=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .python-version | 1 + .../Claude/README.htm | 1717 +++++++++++++++++ .../93. Restore IP Addresses/Claude/README.md | 327 ++++ .../93. Restore IP Addresses/GPT/README.md | 221 +++ .../GPT/Restore_IP_Addresses_js.ipynb | 187 ++ .../GPT/Restore_IP_Addresses_py.ipynb | 312 +++ .../GPT/Restore_IP_Addresses_ts.ipynb | 197 ++ prettier.config.cjs | 13 + prettier.config.js | 12 - 9 files changed, 2975 insertions(+), 12 deletions(-) create mode 100644 .python-version create mode 100644 Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.htm create mode 100644 Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.md create mode 100644 Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/README.md create mode 100644 Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_js.ipynb create mode 100644 Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_py.ipynb create mode 100644 Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_ts.ipynb create mode 100644 prettier.config.cjs delete mode 100644 prettier.config.js diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..dd6a2206 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.4 \ No newline at end of file diff --git a/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.htm b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.htm new file mode 100644 index 00000000..6a9cd528 --- /dev/null +++ b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.htm @@ -0,0 +1,1717 @@ + + + + + + LeetCode 93: Restore IP Addresses - DFS + 枝刈り解説 + + + + + + + + + + + + + + + +
+

+ アルゴリズム概要 +

+ +

問題説明

+

+ 数字のみから成る文字列 + s + に対し、3つのドットを挿入して4つのセグメントを作り、全ての有効なIPv4アドレスを列挙します。 +

+ +

制約条件

+ + +

入出力例

+
+

例1:

+
Input: s = "25525511135"
+Output: ["255.255.11.135","255.255.111.35"]
+
+ +
+

例2:

+
Input: s = "0000"
+Output: ["0.0.0.0"]
+
+ +

戦略のポイント

+ +
+ +
+

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

+
+
+ +
+

+ Python実装 +

+
from __future__ import annotations
+from typing import List
+
+class Solution:
+    """
+    Restore IP Addresses(メモリ最適化版)
+    - 0..255 の文字列を事前キャッシュして、部分文字列生成を回避
+    - 固定長配列 path を再利用し、探索中の一時オブジェクトを最小化
+    """
+
+    # 共有キャッシュ:0〜255 を文字列化して再利用
+    _SEG_CACHE: List[str] = [str(i) for i in range(256)]
+
+    def restoreIpAddresses(self, s: str) -> List[str]:
+        """
+        全ての有効なIPv4アドレスを列挙
+
+        Args:
+            s: 数字のみから成る文字列
+
+        Returns:
+            生成可能な全ての有効IPv4アドレス(順不同)
+
+        Raises:
+            TypeError: 入力がstrでない、または数字以外を含む場合
+
+        Complexity:
+            Time: O(1)(n≤20、最大3^4分岐、出力を除く)
+            Space: O(1)(path固定長のみ、出力を除く)
+        """
+        # 入力検証
+        if not isinstance(s, str):
+            raise TypeError("Input must be a string.")
+
+        n: int = len(s)
+
+        # 数字のみ許可
+        for ch in s:
+            if ch < '0' or ch > '9':
+                raise TypeError("Input must contain digits only.")
+
+        # IPv4は合計4〜12桁のみ成立
+        if n < 4 or n > 12:
+            return []
+
+        res: List[str] = []
+        path: List[str] = [""] * 4  # 固定長配列・再利用
+        SEG = self._SEG_CACHE  # ローカル束縛で属性探索を削減
+
+        def dfs(idx: int, seg: int) -> None:
+            """
+            深さ優先探索でセグメントを決定
+
+            Args:
+                idx: 現在の文字位置
+                seg: 埋まったセグメント数(0〜4)
+            """
+            # 基底条件:4セグメント完成
+            if seg == 4:
+                if idx == n:
+                    # 全文字使い切り → 有効なIP
+                    res.append(".".join(path))
+                return
+
+            remain_segs = 4 - seg
+            remain_chars = n - idx
+
+            # 枝刈り:残文字数が不足または過剰
+            if remain_chars < remain_segs or remain_chars > remain_segs * 3:
+                return
+
+            # 先頭が '0' なら長さ1のみ許可
+            first_is_zero = s[idx] == '0'
+            max_len = 1 if first_is_zero else 3
+
+            val = 0  # セグメント数値を逐次生成
+            for length in range(1, max_len + 1):
+                if idx + length > n:
+                    break
+
+                # 逐次数値化:val = val*10 + digit
+                val = val * 10 + (ord(s[idx + length - 1]) - 48)
+
+                # 255超過したら以降は全て不正
+                if val > 255:
+                    break
+
+                # キャッシュから文字列参照(スライス生成なし)
+                path[seg] = SEG[val]
+                dfs(idx + length, seg + 1)
+
+        dfs(0, 0)
+        return res
+
+ +
+

+ フローチャート +

+
+ + + + + + + + + + + + + + + + + + + + 開始: dfs(0, 0) + + + + + + + + + seg == 4? + + + + + + はい + + + + + + idx == n? + + + + + + はい + + + + + 結果に追加 + res.append() + + + + + + いいえ(文字余り) + + + + + 戻る + + + + + + いいえ + + + + + + remainSegs <= + remainChars <= + remainSegs*3? + + + + + + いいえ(枝刈り) + + + + + 戻る + + + + + + はい + + + + + max_len決定 + (先頭0なら1のみ) + + + + + + + + 長さlen=1..max_len + を試行 + + + + + + + + 逐次数値化 + val = val*10 + digit + + + + + + + + val <= 255? + + + + + + いいえ + + + + + ループ脱出 + + + + + + はい + + + + + path[seg] = CACHE[val] + dfs(idx+len, seg+1) + + + + + + 次の長さ + + +
+ +

+ フローの説明:
+ 1. 基底条件:4セグメント完成 + 全文字使用 → 結果に追加
+ 2. 枝刈り:残文字数が不足/過剰なら即座に戻る
+ 3. 長さ決定:先頭が '0' なら長さ1のみ、それ以外は1〜3を試行
+ 4. 逐次数値化:ループ内で桁を加算(int()変換を回避)
+ 5. 255チェック:超過したらループ脱出(以降は全て不正)
+ 6. 再帰呼び出し:キャッシュから文字列参照し、次のセグメントへ
+ 7. ループバック:次の長さを試行、全て試したら前のステップへ戻る +

+
+ +
+

+ 計算量分析 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ 指標 + + 値 + + 備考 +
+ 時間計算量 + O(1) + 入力長 + n≤20、各セグメント1〜3桁で最大3^4=81分岐だが、枝刈りで実際は大幅削減。出力サイズを除けば定数時間 +
+ 空間計算量 + O(1) + 固定長配列 path[4] + のみ使用。再帰深さ4も定数。キャッシュはクラス変数で共有 +
+ 最適化手法 + + キャッシュ + 枝刈り + + スライス生成ゼロ、逐次数値化、残文字数の上下限チェック +
+
+ +

他手法との比較

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ アプローチ + + 時間 + + 空間 + + 備考 +
+ DFS + 強枝刈り(本実装) + O(1)O(1) + 最速。残文字数/先頭0/255超で剪定 +
+ 3ドット全列挙(i<j<k) + O(n³)O(1) + 実装容易だが条件判定が散在 +
BFS 層展開O(1)O(k) + 中間配列が増えGC圧上昇 +
+
+
+ + + + + + + + + + + + diff --git a/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.md b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.md new file mode 100644 index 00000000..3510ee9f --- /dev/null +++ b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/Claude/README.md @@ -0,0 +1,327 @@ +# Restore IP Addresses - 全有効IPv4アドレスの列挙 + +

目次

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

概要

+ +与えられた数字文字列 `s` に対し、**3つのドット**を挿入して**4つのセグメント**を作り、全ての有効なIPv4アドレスを列挙する問題。 + +**要件**: +- 各セグメントは 0〜255 の整数(先頭ゼロ禁止、ただし単独の `"0"` は許可) +- 文字の順序変更・削除は不可(挿入のみ) +- 有効な組み合わせを全て返す(順不同) + +**制約**: +- `1 <= s.length <= 20` +- `s` は数字のみ + +--- + +

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

+ +- **戦略**:深さ優先探索(DFS)+ 強い枝刈り + - 各セグメントは1〜3桁を試行 + - 残文字数の上下限チェックで早期枝刈り + - 先頭ゼロ・255超過で即座に棄却 +- **データ構造**: + - 固定長配列 `path[4]` でセグメントを再利用 + - 事前キャッシュ `_SEG_CACHE[0..255]` で部分文字列生成を回避 +- **計算量**: + - 時間:O(1)(n≤20、実質的には最大 3^4=81 分岐だが強い枝刈りで大幅削減) + - 空間:O(1)(出力を除く) +- **メモリ最適化**: + - スライス生成を一切行わず、キャッシュ文字列への参照のみ使用 + - 探索中の一時オブジェクト生成をほぼゼロに + +--- + +

図解

+ +### フローチャート:DFS探索の流れ + +```mermaid +flowchart TD + Start[Start: idx=0, seg=0] --> Check{seg == 4?} + Check -- Yes --> Final{idx == n?} + Final -- Yes --> AddRes[Append result to res] + Final -- No --> Return1[Return without adding] + Check -- No --> Prune{Remaining chars valid?} + Prune -- No --> Return2[Return early prune] + Prune -- Yes --> TryLen[Try length 1 to max_len] + TryLen --> BuildVal[Build value digit by digit] + BuildVal --> CheckVal{val <= 255?} + CheckVal -- No --> BreakLoop[Break loop] + CheckVal -- Yes --> SetPath[Set path segment from cache] + SetPath --> Recurse[Recurse: idx+len, seg+1] + Recurse --> Check + AddRes --> Return3[Return] + Return1 --> End[End] + Return2 --> End + Return3 --> End + BreakLoop --> End +``` + +**説明**: +- 深さ4のDFSで各セグメントを決定 +- 残文字数が `[remainSegs, remainSegs*3]` の範囲外なら早期リターン +- 先頭が `'0'` なら長さ1のみ試行(`max_len=1`) +- 逐次数値化(`val = val*10 + digit`)で255超過を検出したら即座にループ脱出 +- 有効なセグメントはキャッシュから参照して `path` に格納 + +### データフロー図 + +```mermaid +graph LR + subgraph Input_Validation + A[Input string s] --> B[Check length 4 to 12] + B --> C[Check digits only] + end + subgraph DFS_Core + C --> D[Initialize path array] + D --> E[DFS with pruning] + E --> F[Try segment lengths] + F --> G[Validate value range] + G --> H[Use cached strings] + end + subgraph Output + H --> I[Collect valid IPs] + I --> J[Return result list] + end +``` + +**説明**: +- 入力検証で範囲外・不正文字を早期棄却 +- DFSコアで効率的な探索(枝刈り + キャッシュ参照) +- 結果リストに有効なIPのみ蓄積 + +--- + +

正しさのスケッチ

+ +**不変条件**: +- `path[0..seg-1]` は全て有効なセグメント(0〜255、先頭ゼロ条件満足) +- `idx` は現在処理中の文字位置、`seg` は埋まったセグメント数 + +**網羅性**: +- 各セグメントで可能な長さ(1〜3、先頭ゼロなら1のみ)を全て試行 +- 枝刈りは「解が存在しない」ケースのみを除外(残文字不足/過剰、255超過) + +**基底条件**: +- `seg == 4` かつ `idx == n`:全文字を使い切って4セグメント完成 → 解として追加 +- `seg == 4` だが `idx < n`:文字が余る → 不正 + +**終了性**: +- `idx` は単調増加、`seg` も単調増加 +- 最大深さ4で再帰終了 + +--- + +

計算量

+ +| 指標 | 値 | 備考 | +|------|-----|------| +| **時間計算量** | **O(1)** | 入力長 n≤20、各セグメント1〜3桁で最大3^4=81分岐だが、枝刈りで実際は大幅削減。出力サイズを除けば定数時間 | +| **空間計算量** | **O(1)** | 固定長配列 `path[4]` のみ使用。再帰深さ4も定数。キャッシュ `_SEG_CACHE` はクラス変数で共有 | + +**最適化の効果**: +- **スライス生成ゼロ**:`s[idx:idx+len]` を作らず、キャッシュ `_SEG_CACHE[val]` への参照のみ +- **逐次数値化**:`ord()` でループ内で桁を加算、`int()` 変換を回避 +- **早期枝刈り**:残文字数の上下限で不可能な分岐を即座に排除 + +--- + +

Python実装

+ +```python +from __future__ import annotations +from typing import List, TYPE_CHECKING + +class Solution: + """ + Restore IP Addresses(メモリ最適化版) + - 0..255 の文字列を事前キャッシュして、部分文字列生成を回避 + - 固定長配列 path を再利用し、探索中の一時オブジェクトを最小化 + """ + + # 共有キャッシュ:0〜255 を文字列化して再利用 + _SEG_CACHE: List[str] = [str(i) for i in range(256)] + + def restoreIpAddresses(self, s: str) -> List[str]: + """ + 全ての有効なIPv4アドレスを列挙 + + Args: + s: 数字のみから成る文字列 + + Returns: + 生成可能な全ての有効IPv4アドレス(順不同) + + Raises: + TypeError: 入力がstrでない、または数字以外を含む場合 + + Complexity: + Time: O(1)(n≤20、最大3^4分岐、出力を除く) + Space: O(1)(path固定長のみ、出力を除く) + """ + # 入力検証 + if not isinstance(s, str): + raise TypeError("Input must be a string.") + + n: int = len(s) + + # 数字のみ許可 + for ch in s: + if ch < '0' or ch > '9': + raise TypeError("Input must contain digits only.") + + # IPv4は合計4〜12桁のみ成立 + if n < 4 or n > 12: + return [] + + res: List[str] = [] + path: List[str] = [""] * 4 # 固定長配列・再利用 + SEG = self._SEG_CACHE # ローカル束縛で属性探索を削減 + + def dfs(idx: int, seg: int) -> None: + """ + 深さ優先探索でセグメントを決定 + + Args: + idx: 現在の文字位置 + seg: 埋まったセグメント数(0〜4) + """ + # 基底条件:4セグメント完成 + if seg == 4: + if idx == n: + # 全文字使い切り → 有効なIP + res.append(".".join(path)) + return + + remain_segs = 4 - seg + remain_chars = n - idx + + # 枝刈り:残文字数が不足または過剰 + if remain_chars < remain_segs or remain_chars > remain_segs * 3: + return + + # 先頭が '0' なら長さ1のみ許可 + first_is_zero = s[idx] == '0' + max_len = 1 if first_is_zero else 3 + + val = 0 # セグメント数値を逐次生成 + for length in range(1, max_len + 1): + if idx + length > n: + break + + # 逐次数値化:val = val*10 + digit + val = val * 10 + (ord(s[idx + length - 1]) - 48) + + # 255超過したら以降は全て不正 + if val > 255: + break + + # キャッシュから文字列参照(スライス生成なし) + path[seg] = SEG[val] + dfs(idx + length, seg + 1) + + dfs(0, 0) + return res +``` + +**主要ステップ**: +1. 入力検証(長さ・文字種) +2. DFS開始(`idx=0, seg=0`) +3. 各セグメントで1〜3桁を試行(先頭ゼロなら1のみ) +4. 残文字数の上下限で枝刈り +5. 逐次数値化で255超過を検出したらループ脱出 +6. 有効なセグメントはキャッシュから参照して `path` に格納 +7. 4セグメント完成時、全文字使い切りなら結果に追加 + +--- + +

CPython最適化ポイント

+ +1. **スライス回避**: + - `s[idx:idx+len]` の代わりに `_SEG_CACHE[val]` への参照のみ + - 探索中の一時 `str` オブジェクト生成をほぼゼロに + +2. **属性アクセス削減**: + - `SEG = self._SEG_CACHE` でローカル変数に束縛 + - ループ内での属性探索コストを削減 + +3. **逐次数値化**: + - `val = val * 10 + (ord(s[i]) - 48)` で桁を加算 + - `int(s[idx:idx+len])` の変換コストを回避 + +4. **固定長配列の再利用**: + - `path: List[str] = [""] * 4` で固定長確保 + - push/pop せずインデックス代入のみでV8(CPython)に最適 + +5. **早期枝刈り**: + - 残文字数の上下限チェックで不可能な分岐を即座に排除 + - `val > 255` で即座にループ脱出 + +**結果**: +- LeetCode上で **Runtime 0ms(100%)**、**Memory 17.79MB(76.42%)** を達成 +- メモリは出力リスト自体のサイズも含むため、解の個数が多い入力では不可避に増加 + +--- + +

エッジケースと検証観点

+ +| ケース | 入力例 | 期待出力 | 検証ポイント | +|--------|--------|----------|--------------| +| **最小長** | `"1111"` | `["1.1.1.1"]` | 4文字で1解のみ | +| **全ゼロ** | `"0000"` | `["0.0.0.0"]` | 先頭ゼロ許可(単独"0") | +| **長さ不足** | `"123"` | `[]` | n<4で即座に空リスト | +| **長さ過剰** | `"1"*13` | `[]` | n>12で即座に空リスト | +| **先頭ゼロ** | `"010010"` | `["0.10.0.10", "0.100.1.0"]` | 先頭ゼロは長さ1のみ | +| **255境界** | `"25525511135"` | `["255.255.11.135", "255.255.111.35"]` | 255は有効、256は不正 | +| **複数解** | `"101023"` | 5解(例題参照) | 全分岐の網羅性 | +| **不正文字** | `"12a34"` | `TypeError` | 数字以外を含む入力 | + +**検証観点**: +- 残文字数の上下限枝刈りが正しく機能するか +- 先頭ゼロの処理(`max_len=1`)が正しいか +- 逐次数値化で255超過を正しく検出するか +- キャッシュ参照が元の桁列と一致するか + +--- + +

FAQ

+ +**Q1: なぜスライスを避けるのか?** +- A: `s[idx:idx+len]` は毎回新しい `str` オブジェクトを生成し、GC圧が高まる。キャッシュ参照なら既存オブジェクトの再利用で割り当てゼロ。 + +**Q2: `_SEG_CACHE` はどれくらいメモリを使うか?** +- A: 256個の文字列(`"0"`〜`"255"`)で合計約2KB程度。クラス変数として共有されるため、インスタンスごとの追加コストはゼロ。 + +**Q3: LeetCodeのMemoryスコアが100%にならない理由は?** +- A: 出力リスト自体のサイズが含まれるため、解の個数が多い入力では不可避に増加。中間オブジェクトは最小化済み。 + +**Q4: 先頭ゼロの処理が正しいか?** +- A: `first_is_zero` で先頭が `'0'` なら `max_len=1` に制限。`"01"` や `"001"` は試行されない。 + +**Q5: 枝刈りの効果は?** +- A: 残文字数の上下限チェックで、不可能な分岐(例:残り1文字で2セグメント必要)を即座に排除。実測で探索回数を大幅削減。 + +**Q6: 他のアプローチと比較すると?** +- A: 3重ループ(ドット位置総当り)は実装容易だが条件判定が散在。DFS + 枝刈りは制御フローが明確で高速。 + +**Q7: ジェネレータにできないか?** +- A: LeetCodeの関数シグネチャは `List[str]` 返却固定。プロダクションなら `yield` で逐次処理可能。 + +**Q8: 並列化は有効か?** +- A: 探索空間が小さい(最大3^4)ため、GILのオーバーヘッドで逆に遅くなる。単一スレッドで十分。 \ No newline at end of file diff --git a/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/README.md b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/README.md new file mode 100644 index 00000000..b423419a --- /dev/null +++ b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/README.md @@ -0,0 +1,221 @@ +# Restore IP Addresses - DFSと強力な枝刈りで全列挙 + +## Table of Contents + +* [概要](#overview) +* [アルゴリズム要点(TL;DR)](#tldr) +* [図解](#figures) +* [正しさのスケッチ](#correctness) +* [計算量](#complexity) +* [Python 実装](#impl) +* [CPython最適化ポイント](#cpython) +* [エッジケースと検証観点](#edgecases) +* [FAQ](#faq) + +

概要

+ +* **プラットフォーム/ID**: LeetCode 93 +* **タイトル**: Restore IP Addresses +* **要約**: 数字のみの文字列 `s` に 3 個のドットを挿入して、**有効な IPv4 アドレス**(4 セグメント、各 0〜255、先頭ゼロ不可。ただし単独の "0" は可)を**すべて**列挙する。 +* **入出力仕様(簡潔)** + + * 入力: `s: str`(数字のみ、長さ 1〜20 だが有効解は 4〜12 桁に限定) + * 出力: `List[str]`(順不同の有効 IPv4 一覧) +* **想定データ構造**: String +* **代表例** + + * `s="25525511135"` → `["255.255.11.135","255.255.111.35"]` + * `s="0000"` → `["0.0.0.0"]` + * `s="101023"` → `["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]` +* **関数シグネチャ(LeetCode準拠)** + + * `class Solution: def restoreIpAddresses(self, s: str) -> List[str]:` + +

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

+ +* **戦略**: 深さ 4 の DFS でセグメント長を 1〜3 の範囲で試す。 +* **枝刈り**: + + * 残文字数 `remainChars` と残セグメント数 `remainSegs` で **下限/上限**チェック:`remainSegs ≤ remainChars ≤ 3*remainSegs` + * 先頭 `'0'` は **長さ1のみ**許可(leading zero 禁止)。 + * 数値が **255 を超えたら打ち切り**(以降の長い桁は必ず不適)。 +* **データ構造**: 一時バッファ `path[4]` を再利用して `'.'.join(path)` で完成。 +* **メモリ最適化**: `0..255` の文字列表現を **事前キャッシュ**して **部分文字列の slice を作らない**。 +* **計算量目標**: Time (O(1))(実質 3^4 分岐、n≤12) / Space (O(1))(出力除く)。 + +

図解

+ +**フローチャート(探索と枝刈り)** + +```mermaid +flowchart TD + Start[Start dfs idx seg] --> Base{seg == 4} + Base -- Yes --> CheckEnd{idx == n} + CheckEnd -- Yes --> Push[append join path] + CheckEnd -- No --> Ret1[return] + Base -- No --> Bounds{remainChars range ok} + Bounds -- No --> Ret2[return] + Bounds -- Yes --> Zero{first char is 0} + Zero -- Yes --> Try1[try length 1] + Zero -- No --> Try3[try length 1..3] + Try1 --> Build1[accumulate val] + Try3 --> Build3[accumulate val] + Build1 --> Over1{val > 255} + Build3 --> Over3{val > 255} + Over1 -- Yes --> Ret3[break] + Over1 -- No --> Place1[place cache val] + Over3 -- Yes --> Ret4[break] + Over3 -- No --> Place3[place cache val] + Place1 --> Recur1[dfs next] + Place3 --> Recur3[dfs next] + Recur1 --> Base + Recur3 --> Base +``` + +*説明*: 深さ 4 の再帰で各セグメント長を試し、**残文字数の範囲**・**先頭ゼロ**・**255 超過**で枝刈りする。 + +**データフロー(前処理→DFS→出力)** + +```mermaid +graph LR + A[Input s] --> B[Validate digits] + B --> C[Length 4..12 check] + C --> D[Init path and cache] + D --> E[DFS with pruning] + E --> F[Build results] + F --> G[Return list] +``` + +*説明*: 入力検証の後、固定配列 `path` と数値→文字列キャッシュで**中間オブジェクト生成を抑制**しながら列挙。 + +

正しさのスケッチ

+ +* **網羅性**: 各セグメントの長さを 1〜3 の全可能性について試すため、4 セグメント分の全挿入位置を漏れなく探索。 +* **有効性保持**: + + * 先頭ゼロ規則(単独 "0" のみ)を満たすよう `max_len` を調整。 + * 数値は逐次加算で評価し、255 を超えた時点で以降の長さは不可能なので打ち切り。 + * 残文字数の範囲チェックにより、**必ず4セグメントを消費できる**構成のみ前進。 +* **基底条件**: `seg==4` のときに `idx==n` ならのみ解として採用。 +* **終了性**: 深さは最大 4、各段の分岐は最大 3、有限回で必ず終了。 + +

計算量

+ +* **Time**: (O(1))(n≤12、各段最大 3 分岐で高々 (3^4=81) 展開。出力サイズを除く) +* **Space**: (O(1))(固定長 `path` のみ、結果配列を除く) + +

Python 実装

+ +> * LeetCode の関数シグネチャ準拠 +> * `from __future__ import annotations`・型注釈あり(pylance 対応) +> * **Pure**(外部副作用なし)。`0..255` の文字列はクラスキャッシュで再利用 + +```python +from __future__ import annotations +from typing import List, Final + +class Solution: + """ + Restore IP Addresses + 与えられた数字文字列に3つのドットを挿入し、全ての有効なIPv4アドレスを列挙する。 + - 4セグメント、各セグメントは 0..255 + - 先頭ゼロは禁止(ただし単独 "0" は可) + - 文字の順序変更・削除は不可 + """ + + # 共有キャッシュ:0..255 の文字列表現(部分文字列 slice を避けるため) + _SEG_CACHE: Final[List[str]] = [str(i) for i in range(256)] + + def restoreIpAddresses(self, s: str) -> List[str]: + """ + Args: + s: 数字のみから成る文字列 + Returns: + 生成可能な全ての有効なIPv4文字列(順不同) + Raises: + TypeError: s が str でない、または数字以外が含まれる場合 + + Complexity: + Time: O(1)(n<=12・最大3^4分岐、出力を除く) + Space: O(1)(path固定長のみ、出力を除く) + """ + if not isinstance(s, str): + raise TypeError("Input must be a string.") + + n: int = len(s) + + # 入力が数字のみかを検証(LeetCode前提外にも堅牢化) + for ch in s: + if ch < '0' or ch > '9': + raise TypeError("Input must contain digits only.") + + # 合法なIPv4は 4..12 桁のみ + if n < 4 or n > 12: + return [] + + res: List[str] = [] + path: List[str] = [""] * 4 # 固定長バッファを再利用 + SEG = self._SEG_CACHE # ローカル束縛で属性アクセスを削減 + + def dfs(idx: int, seg: int) -> None: + # 4セグメントを決め終えたら、全体を消費しているか確認 + if seg == 4: + if idx == n: + res.append(".".join(path)) + return + + remain_segs: int = 4 - seg + remain_chars: int = n - idx + + # 下限・上限の枝刈り(残文字が少なすぎる/多すぎる) + if remain_chars < remain_segs or remain_chars > remain_segs * 3: + return + + # 先頭ゼロなら長さ1のみ許可 + first_is_zero: bool = s[idx] == '0' + max_len: int = 1 if first_is_zero else 3 + + val: int = 0 # セグメント値を逐次数値化 + for length in range(1, max_len + 1): + if idx + length > n: + break + # val = val * 10 + digit + val = val * 10 + (ord(s[idx + length - 1]) - 48) + if val > 255: + break # これ以降は必ず255超過 + + # slice を作らず、キャッシュ文字列を参照 + path[seg] = SEG[val] + dfs(idx + length, seg + 1) + + dfs(0, 0) + return res +``` + +

CPython最適化ポイント

+ +* **slice 回避**: `s[idx:idx+len]` を作らず、`0..255` の文字列をクラスキャッシュから参照。 +* **逐次数値化**: `int()` ではなく `val = val*10 + (ord(ch)-48)` により、途中の短命オブジェクトを削減。 +* **ローカル束縛**: `SEG = self._SEG_CACHE`、`n = len(s)` などをローカルに保持して属性探索を減らす。 +* **固定長バッファ**: `path` は長さ 4 を再利用し、`'.'.join(path)` のみで最終文字列生成。 +* **再帰の浅さ**: 深さ最大 4 のため、関数呼び出しコストとスタック使用は極小。 + +

エッジケースと検証観点

+ +* **長さ外**: `len(s) < 4` または `> 12` は空リスト。 +* **全ゼロ**: `"0000"` → `"0.0.0.0"` のみ。 +* **先頭ゼロ混在**: `"010010"` → `"0.10.0.10","0.100.1.0"` など、先頭ゼロ規則が守られていること。 +* **上限値**: `"25525511135"` の代表解が含まれること。 +* **255超過の剪定**: `"256..."` のようなパスは早期に打ち切られること。 +* **非数字の防御**: 入力が数字以外を含む場合に `TypeError` が送出されること。 + +

FAQ

+ +* **Q. メモリ消費が気になります。もっと減らせますか?** + **A.** 出力リストは問題仕様上不可避ですが、上記実装は探索中の **部分文字列 slice を完全に排除**し、中間 `str` 生成を大幅に削減しています。これが CPython での実用的な最小化です。 + +* **Q. 反復で書けますか?** + **A.** 可能ですが、深さ 4 の軽量再帰は Python でも十分高速で読みやすいです。反復化のメリットは限定的です。 + +* **Q. `parseInt` 相当の `int(s[i:j])` を使わない理由は?** + **A.** 毎回新しい `str` スライスが必要になりオーバーヘッドが増えます。逐次数値化はその生成を避けられます。 diff --git a/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_js.ipynb b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_js.ipynb new file mode 100644 index 00000000..652d352d --- /dev/null +++ b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_js.ipynb @@ -0,0 +1,187 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ef2c57ad", + "metadata": {}, + "source": [ + "\n", + "---\n", + "\n", + "# 0. 実行環境\n", + "\n", + "* **Language/Runtime:** JavaScript (Node.js v22.14.0)\n", + "* **Module:** CommonJS\n", + "* **外部ライブラリ:** 使用不可(Node 標準のみ)\n", + "* **CI 前提:** `node solution.js` 実行可能(I/O なし/関数のみ)\n", + "\n", + "---\n", + "\n", + "# 1. 問題の分析\n", + "\n", + "## 競技プログラミング視点\n", + "\n", + "* 4 セグメント(各 1〜3 桁、0〜255、先頭 0 禁止)という**強い制約**があるため、深さ最大 4 の探索で完全探索しても軽い。\n", + "* 文字列長は有効 IP では **4〜12** に限定される(これ以外は即時空配列)。\n", + "* 各セグメントは長さ候補が 1〜3 の **最大 3 分岐**。よって理論上の探索ノードは高々 (3^4=81) 程度+枝刈りでさらに減る。\n", + "\n", + "## 業務開発視点\n", + "\n", + "* **保守性**:`isValidSegment` のような小関数に分けず、ホットパスのクロージャ生成を抑えるため **1 関数内で判定を完結**。\n", + "* **例外方針**:問題前提外(非文字列・非数字)には `TypeError` を投げる。範囲外長は業務上は空配列返す方が扱いやすい(例外ではない)。\n", + "\n", + "## JavaScript特有の考慮点\n", + "\n", + "* **V8 最適化**:数値化は `charCodeAt` を用いた逐次加算で `parseInt` を回避し、**分岐ごとに最大 3 回**の軽量演算に抑制。\n", + "* **GC 対策**:結果バッファ `path` を**固定長配列(長さ4)** で再利用。部分文字列は `len===1` のときは `s[idx]` を使用して短命オブジェクトを最小化。\n", + "* **ループ**:`for` ベースで単純化、スプレッド/高階関数は使用しない。\n", + "\n", + "---\n", + "\n", + "# 2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | JS実装コスト | 可読性 | 備考 |\n", + "| ------------------- | ------------------------------------ | -----------------: | ------: | --: | --------------------- |\n", + "| 方法A: DFS+枝刈り(本採用) | 高々 (3^4) ≒ **O(1)**(入力長で見ても O(n) 未満) | **O(1)**(深さ4・固定配列) | 低 | 中 | 長さ制約と 0/255 判定で強力に枝刈り |\n", + "| 方法B: 3ドット全探索(i255)**で無駄探索をほぼ排除。\n", + "* **JavaScript特有の最適化ポイント:**\n", + "\n", + " * `charCodeAt` による**逐次数値化**(`val = val*10 + digit`)。\n", + " * `path` は**固定長 4** を再利用、都度 new しない。\n", + " * 早期長チェック(`n<4 || n>12`)で**即 return**。\n", + "\n", + "---\n", + "\n", + "# 4. コード実装(solution.js) — *LeetCode 形式(Pure Function/I/Oなし)*\n", + "\n", + "```javascript\n", + "'use strict';\n", + "\n", + "/**\n", + " * Restore IP Addresses (LeetCode)\n", + " * 純関数・I/Oなし。入力は数字文字列のみを想定。\n", + " *\n", + " * @param {string} s - 数字のみから成る文字列\n", + " * @returns {string[]} 生成可能な全ての有効なIPv4文字列(順不同)\n", + " * @throws {TypeError} 引数が文字列でない/数字以外を含む場合\n", + " * @complexity\n", + " * 時間: 高々 3^4 分岐 + 軽量チェックで実質 O(1)(n<=12 かつ各分岐で最大3桁のみ)\n", + " * 空間: O(1) 追加(深さ4の固定配列 path のみ、結果配列は出力)\n", + " */\n", + "var restoreIpAddresses = function (s) {\n", + " // ---- 入力検証(軽量 & 早期)----\n", + " if (typeof s !== 'string') throw new TypeError('Input must be a string.');\n", + " // 問題前提外にも耐える:数字のみ許可\n", + " // (LeetCode想定では必ず数字だが、業務コードとして堅牢化)\n", + " for (let i = 0, n = s.length; i < n; i++) {\n", + " const c = s.charCodeAt(i);\n", + " if (c < 48 || c > 57) throw new TypeError('Input must contain digits only.');\n", + " }\n", + "\n", + " const n = s.length;\n", + " // 有効なIPv4の総桁数は 4〜12(各セグメント1〜3桁)\n", + " if (n < 4 || n > 12) return [];\n", + "\n", + " const res = [];\n", + " const path = new Array(4); // セグメント4つを固定長で再利用\n", + "\n", + " /**\n", + " * DFS(idx: 次に読む位置, seg: 何個目のセグメントか)\n", + " * 残文字数と残セグメント数で強い枝刈りを行う。\n", + " */\n", + " function dfs(idx, seg) {\n", + " if (seg === 4) {\n", + " if (idx === n) res.push(path.join('.'));\n", + " return;\n", + " }\n", + "\n", + " const remainSegs = 4 - seg;\n", + " const remainChars = n - idx;\n", + " // 残文字が足りない/多すぎる場合は即棄却\n", + " if (remainChars < remainSegs || remainChars > remainSegs * 3) return;\n", + "\n", + " let val = 0;\n", + "\n", + " // len: 現セグメントの長さ(1〜3)\n", + " // 先頭が '0' の場合は \"0\" のみ許可(leading zero 禁止)\n", + " const firstIsZero = s.charCodeAt(idx) === 48;\n", + " const maxLen = firstIsZero ? 1 : 3;\n", + "\n", + " for (let len = 1; len <= maxLen && idx + len <= n; len++) {\n", + " // 数値を逐次形成(parseInt回避)\n", + " val = val * 10 + (s.charCodeAt(idx + len - 1) - 48);\n", + " if (val > 255) break; // 255 超過で以降の長い桁も全て超過\n", + "\n", + " // 文字列化は必要最小限(len===1 は s[idx] をそのまま)\n", + " path[seg] = (len === 1) ? s[idx] : s.slice(idx, idx + len);\n", + " dfs(idx + len, seg + 1);\n", + " }\n", + " }\n", + "\n", + " dfs(0, 0);\n", + " return res;\n", + "};\n", + "\n", + "// Node実行/テスト環境用(LeetCodeでは無視されても害はない)\n", + "if (typeof module !== 'undefined') {\n", + " module.exports = restoreIpAddresses;\n", + "}\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "\n", + "Memory 54.24 MB\n", + "Beats 91.44%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "# 5. 追加メモ(JS最適化チェックリスト)\n", + "\n", + "* ループは `for` を基本、`map/forEach` 不使用で**割り当て削減**。\n", + "* `path` は**固定長再利用**、`Array.join('.')` は最後にのみ実行。\n", + "* 先頭ゼロ・範囲(0〜255)・残文字数は**早期枝刈り**で探索爆発を防止。\n", + "* 数値化は `charCodeAt` を使い**逐次演算**で `parseInt` 回避。\n", + "* 例外は**ホットパス外(入口)**で早期に弾く。\n", + "\n", + "---\n", + "\n", + "必要に応じて、同アプローチの **3 ドット全探索版(i255)により実際はさらに小さい。\n", + "* **計算量**:入力長 (n) は有効ケースでも最大12。事実上**定数時間**(出力サイズを除く)。\n", + "* **メモリ**:深さ4のパス配列を**再利用**して **O(1)**(出力除く)。\n", + "\n", + "### 業務開発視点\n", + "\n", + "* **可読性/保守性**:`dfs(idx, seg)` に枝刈り条件を近接配置。命名とコメントで条件根拠を明確化。\n", + "* **型安全性/エラーハンドリング**:`s` は `str`、**数字のみ**を実行時ガード。前提外は `TypeError` を送出。\n", + "* **副作用なし**:Pureに徹し、入力破壊なし。\n", + "\n", + "### Python特有考慮\n", + "\n", + "* **CPython特性**:`ord()` を使った逐次数値化(`val = val*10 + digit`)で `int(s[..])` 多用を回避。\n", + "* **ローカル変数束縛**:ホットパスで `n`, `res`, `path`, `s` をローカル参照し属性探索を減らす。\n", + "* **文字列操作**:`len==1` は `s[idx]` を直接使用し、不要なスライスを抑制。\n", + "\n", + "---\n", + "\n", + "## 2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 |\n", + "| ----------------------- | ---------------------- | ---------- | ----------- | --- | --------- | ---------- | ---------------- |\n", + "| **方法A: DFS + 強枝刈り(採用)** | **O(1)**(n≤12, 分岐≤3^4) | **O(1)** | 低 | 高 | 不要 | 適 | 残文字数/先頭0/255超で剪定 |\n", + "| 方法B: 3ドット全列挙(i255` で早期 `break`。\n", + " * `path: list[str]` を**固定長4**で再利用。\n", + " * `remainChars` と `remainSegs` の上下限で**強い枝刈り**。\n", + "\n", + "---\n", + "\n", + "## 4. 検証(観点のみ)\n", + "\n", + "* **境界**:`\"0000\"` → `[\"0.0.0.0\"]`、`\"25525511135\"`、`\"101023\"`。\n", + "* **長さ外**:`n<4`/`n>12` は即 `[]`。\n", + "* **不正入力**:数字以外を含むと `TypeError`。\n", + "\n", + "---\n", + "\n", + "## 5. 実装(LeetCode形式 / Class形式)\n", + "\n", + "```python\n", + "from typing import List\n", + "\n", + "class Solution:\n", + " \"\"\"\n", + " Restore IP Addresses\n", + " 与えられた数字文字列にドットを挿入して、全ての有効なIPv4アドレスを列挙する。\n", + " - 4セグメント・各0〜255・先頭0禁止(ただし単独\"0\"は可)\n", + " - 文字の順序変更や削除は不可(挿入のみ)\n", + " \"\"\"\n", + "\n", + " def restoreIpAddresses(self, s: str) -> List[str]:\n", + " \"\"\"\n", + " Args:\n", + " s: 数字のみから成る文字列\n", + "\n", + " Returns:\n", + " 生成可能な全ての有効IPv4アドレス(順不同)\n", + "\n", + " Raises:\n", + " TypeError: 入力がstrでない、または数字以外を含む場合\n", + "\n", + " Complexity:\n", + " Time: O(1)(n<=12・最大3^4分岐、出力サイズを除く)\n", + " Space: O(1)(深さ4の作業配列のみ、出力を除く)\n", + " \"\"\"\n", + " if not isinstance(s, str):\n", + " raise TypeError(\"Input must be a string.\")\n", + "\n", + " n: int = len(s)\n", + "\n", + " # 数字のみを許可(LeetCodeでは前提だが堅牢化)\n", + " for ch in s:\n", + " if ch < '0' or ch > '9':\n", + " raise TypeError(\"Input must contain digits only.\")\n", + "\n", + " # IPv4は合計4〜12桁のみ成立\n", + " if n < 4 or n > 12:\n", + " return []\n", + "\n", + " res: List[str] = []\n", + " path: List[str] = [\"\"] * 4 # 固定長・再利用\n", + "\n", + " def dfs(idx: int, seg: int) -> None:\n", + " # 4セグメント充足\n", + " if seg == 4:\n", + " if idx == n:\n", + " res.append(\".\".join(path))\n", + " return\n", + "\n", + " remain_segs: int = 4 - seg\n", + " remain_chars: int = n - idx\n", + "\n", + " # 残文字数が不足/過剰なら打ち切り\n", + " if remain_chars < remain_segs or remain_chars > remain_segs * 3:\n", + " return\n", + "\n", + " # 先頭が '0' の場合は長さ1のみ\n", + " first_is_zero: bool = s[idx] == '0'\n", + " max_len: int = 1 if first_is_zero else 3\n", + "\n", + " val: int = 0 # セグメント数値を逐次生成(ordで桁追加)\n", + " for length in range(1, max_len + 1):\n", + " if idx + length > n:\n", + " break\n", + " val = val * 10 + (ord(s[idx + length - 1]) - 48)\n", + " if val > 255:\n", + " break # これ以降は必ず255超過\n", + "\n", + " # 文字列切り出しは必要最小限(length==1は s[idx] を直接)\n", + " path[seg] = s[idx] if length == 1 else s[idx:idx + length]\n", + " dfs(idx + length, seg + 1)\n", + "\n", + " dfs(0, 0)\n", + " return res\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 17.94 MB\n", + "Beats 28.48%\n", + "```\n", + "\n", + "---\n", + "\n", + "## 6. Python特有の追加考慮\n", + "\n", + "* **CPython最適化**:ローカル変数束縛・逐次数値化・早期`break`でバイトコード実行回数を削減。\n", + "* **GIL**:CPUバウンドな並列化は不要(探索空間が小さい)。単一スレッドで十分。\n", + "* **標準ライブラリ**:追加の構造は不要(`deque/heapq`等は不使用が最小コスト)。\n", + "\n", + "---\n", + "\n", + "**LeetCode の「Memory」スコアは出力リスト自体のサイズも含む**ため、**解の個数が多い入力ほど不利**になります(これはアルゴリズム上ほぼ不可避)。\n", + "それでも**一時オブジェクト**をさらに削ってピーク使用量を下げる余地はあります。ポイントは「**部分文字列(slice)を作らない**」ことです。\n", + "\n", + "---\n", + "\n", + "## 改善ポイント(メモリ削減)\n", + "\n", + "1. **セグメント文字列の再利用(重要)**\n", + " `s[idx:idx+len]` のたびに新しい `str` が生成されます。\n", + " → `0..255` の文字列を **事前にプリコンパイル(キャッシュ)** し、検証OKなら **その参照を使い回す** ことで slice 生成をゼロにします。\n", + "\n", + " * 先頭ゼロの禁止(len>1 で `firstIsZero`)を守っている限り、`val`→`SEG[val]` は **元の桁列と同一表記**になります(例: `\"10\"` → `\"10\"`)。\n", + " * `len==1` でも `SEG[val]` を使えば `s[idx]` を作る必要がありません。\n", + "\n", + "2. **`join` 以外の一時生成を作らない**\n", + " `path` は固定長4・再利用。`res.append(\".\".join(path))` の **1回だけ**新規 `str` を作ります。\n", + "\n", + "3. **再帰は深さ4のみ**\n", + " 再帰自体のフレームは小さく、スタック増加は無視できるため現状のままでOKです。\n", + "\n", + "---\n", + "\n", + "## 改善版(LeetCode / Class形式 / 型注釈あり / CPython 3.11)\n", + "\n", + "```python\n", + "from typing import List\n", + "\n", + "class Solution:\n", + " \"\"\"\n", + " Restore IP Addresses(メモリ最適化版)\n", + " - 0..255 の文字列を事前キャッシュして、部分文字列 slice を一切作らない。\n", + " - 検証に通ったらキャッシュ参照を path に格納し、join で最終だけ新規文字列化。\n", + " \"\"\"\n", + "\n", + " # 共有キャッシュ:0..255 を文字列にして再利用\n", + " _SEG_CACHE: List[str] = [str(i) for i in range(256)]\n", + "\n", + " def restoreIpAddresses(self, s: str) -> List[str]:\n", + " \"\"\"\n", + " Args:\n", + " s: 数字のみから成る文字列\n", + " Returns:\n", + " 生成可能な全ての有効IPv4アドレス(順不同)\n", + " Raises:\n", + " TypeError: 入力がstrでない/数字以外を含む場合\n", + "\n", + " Complexity:\n", + " Time: O(1)(n<=12・最大3^4分岐、出力を除く)\n", + " Space: O(1)(path固定長のみ、出力を除く)\n", + " \"\"\"\n", + " if not isinstance(s, str):\n", + " raise TypeError(\"Input must be a string.\")\n", + "\n", + " n: int = len(s)\n", + "\n", + " # 数字のみ許可(LeetCode前提外にも堅牢化)\n", + " for ch in s:\n", + " if ch < '0' or ch > '9':\n", + " raise TypeError(\"Input must contain digits only.\")\n", + "\n", + " if n < 4 or n > 12:\n", + " return []\n", + "\n", + " res: List[str] = []\n", + " path: List[str] = [\"\"] * 4 # 固定長・再利用\n", + " SEG = self._SEG_CACHE # ローカル束縛で属性探索を削減\n", + "\n", + " def dfs(idx: int, seg: int) -> None:\n", + " if seg == 4:\n", + " if idx == n:\n", + " # ここで初めて新しい文字列を1個生成\n", + " res.append(\".\".join(path))\n", + " return\n", + "\n", + " remain_segs = 4 - seg\n", + " remain_chars = n - idx\n", + " if remain_chars < remain_segs or remain_chars > remain_segs * 3:\n", + " return\n", + "\n", + " first_is_zero = s[idx] == '0'\n", + " max_len = 1 if first_is_zero else 3\n", + "\n", + " val = 0\n", + " for length in range(1, max_len + 1):\n", + " # 範囲外は break\n", + " if idx + length > n:\n", + " break\n", + " # 逐次数値化:val = val*10 + digit\n", + " val = val * 10 + (ord(s[idx + length - 1]) - 48)\n", + " if val > 255:\n", + " break # これ以降は必ず255超過\n", + "\n", + " # slice を作らず、キャッシュ文字列を参照\n", + " # leading zero は max_len により既に除外済み\n", + " path[seg] = SEG[val]\n", + " dfs(idx + length, seg + 1)\n", + "\n", + " dfs(0, 0)\n", + " return res\n", + "\n", + "Analyze Complexity\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 17.79 MB\n", + "Beats 76.42%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "## 期待できる効果\n", + "\n", + "* **slice 文字列(`s[idx:idx+len]`)を一切生成しない**ため、\n", + " 探索中に発生する **短命 `str` の割り当てがほぼゼロ** になります。\n", + "* `SEG` の 256 個の `str` は**再利用される単一インスタンス**なので、\n", + " 多数の候補を試す入力でも、**ヒープ断片化を抑制**できます。\n", + "\n", + "> なお、LeetCode のメモリ指標には**最終的な出力リスト**も含まれます。\n", + "> 有効解が多い入力(例: `\"0000\"` の 1 解は少ないが、他のケースでは解が多くなりやすい)では、\n", + "> **結果件数が増えるほどメモリ値は不可避に増加**します。上の最適化は**中間オブジェクト**を削ることでピークを下げるアプローチです。\n", + "\n", + "必要であれば、**出力をジェネレータにして逐次処理**する設計(プロダクション向け)も可能ですが、LeetCode の関数シグネチャは `List[str]` 返却固定なので、上記のような**キャッシュ参照 + 非slice 方針**が実戦的な最適化になります。\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.12.4", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_ts.ipynb b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_ts.ipynb new file mode 100644 index 00000000..096bd93d --- /dev/null +++ b/Algorithm/Backtracking/leetcode/93. Restore IP Addresses/GPT/Restore_IP_Addresses_ts.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fc232d6c", + "metadata": {}, + "source": [ + "\n", + "---\n", + "\n", + "## 1. 問題の分析\n", + "\n", + "### **競技プログラミング視点での分析**\n", + "\n", + "* **制約の強さ**(4セグメント各1–3桁・0〜255・先頭0不可)により、深さ最大4・各段最大3分岐の DFS で十分高速。\n", + "* 文字列長は有効な場合 **4〜12** に限定できるため、これ以外は即座に空配列。\n", + "* **枝刈り**:\n", + "\n", + " * 残文字数と残セグメント数の下限/上限チェック(`remainSegs ≤ remainChars ≤ remainSegs*3`)。\n", + " * セグメント先頭が `'0'` の場合は長さ1のみ許可。\n", + " * 数値が 255 を超えたらそれ以上の長さは全て不適(打ち切り)。\n", + "\n", + "### **業務開発視点での分析**\n", + "\n", + "* **型安全性**:入力は `string` のみ許可、**実行時ガード**で「数字のみ」を検証し例外(`TypeError`)を投げることで堅牢化。\n", + "* **保守性**:ホットパスに余計な関数分割を作らず、**1 関数内の小さなヘルパロジック**とする。結果構築は `path: string[]` を**固定長4**で再利用。\n", + "* **可読性**:セグメント長ループ(1〜3)と枝刈り条件を**近接配置**して意図が読みやすい構造。\n", + "\n", + "### **TypeScript特有の考慮点**\n", + "\n", + "* **厳密な型**:`string` 入力、`string[]` 出力。内部変数も明示型でコンパイラ最適化を支援。\n", + "* **`readonly` の活用**:引数はイミュータブルに扱い、副作用を避ける(pure function)。\n", + "* **null安全**:インデックス演算は境界チェック済みのため `!` やアサーション不要。\n", + "* **実行時型ガード**:`typeof` と `charCodeAt` 数値範囲検査で「数字のみ」を保証。\n", + "\n", + "---\n", + "\n", + "## 2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 |\n", + "| ----------------------- | ------------------------- | ------------------ | ------- | ---- | --- | ---------------- |\n", + "| **方法A:DFS + 強い枝刈り(採用)** | 高々 (3^4) ⇒ **O(1)**(n≤12) | **O(1)**(深さ4・固定配列) | 低 | 高 | 高 | 先頭0・255超・残文字数で剪定 |\n", + "| 方法B:3ドット位置の三重ループ | **O(n³)**(n≤12) | O(1) | 低〜中 | 高 | 中 | 実装単純だが分岐散在 |\n", + "| 方法C:BFS(幅優先) | **O(1)** 同等 | 候補列で O(k) | 中 | 高 | 中 | 中間配列分の GC が増える |\n", + "\n", + "---\n", + "\n", + "## 3. 選択したアルゴリズムと理由\n", + "\n", + "* **選択したアプローチ**: **方法A(DFS + 強い枝刈り)**\n", + "* **理由**:\n", + "\n", + " * 計算量は事実上定数時間。枝刈りで探索空間を極小化。\n", + " * TypeScript でも**単純な制御フロー**で型推論が効き、可読性・保守性が高い。\n", + " * 追加ヒープ確保を抑え、**`path` 再利用**で GC 圧を低下。\n", + "* **TypeScript特有の最適化ポイント**:\n", + "\n", + " * 変数へ**明示型付与**で JIT/最適化の妨げを減らす。\n", + " * `charCodeAt` により `parseInt` を避け、**逐次加算で 255 判定**。\n", + " * `as const`/`readonly` 方針で**副作用抑止**(pure function)。\n", + "\n", + "---\n", + "\n", + "## 4. 実装コード(ESM・Strict・LeetCode形式)\n", + "\n", + "> * **Pure function**:入出力以外に副作用なし\n", + "> * **型安全**:厳密な型注釈と実行時ガード\n", + "> * **外部ライブラリ不使用**:Node 標準のみ\n", + "> * **ESM**:`export` を付与(LeetCode貼付時は関数本体のみでも可)\n", + "\n", + "```ts\n", + "/**\n", + " * Restore IP Addresses\n", + " * 与えられた数字文字列にドットを挿入して、全ての有効な IPv4 を列挙する。\n", + " * - 4 セグメント、各 0〜255、先頭 0 禁止(単独 \"0\" は可)\n", + " * - 文字の順序変更・削除は禁止\n", + " *\n", + " * @param s - 数字のみから成る文字列\n", + " * @returns 生成可能な全ての有効 IPv4 文字列(順不同)\n", + " * @throws {TypeError} 入力が文字列でない/数字以外を含む\n", + " * @complexity Time: O(1)(最大 3^4 分岐, n ≤ 12), Space: O(1)(深さ4・固定配列、出力除く)\n", + " */\n", + "export function restoreIpAddresses(s: string): string[] {\n", + " // ---- 入力検証(軽量 & 早期)----\n", + " if (typeof s !== 'string') {\n", + " throw new TypeError('Input must be a string.');\n", + " }\n", + "\n", + " const n: number = s.length;\n", + "\n", + " // 数字のみ許可(LeetCode前提外も堅牢に)\n", + " for (let i = 0; i < n; i++) {\n", + " const code = s.charCodeAt(i);\n", + " if (code < 48 || code > 57) {\n", + " throw new TypeError('Input must contain digits only.');\n", + " }\n", + " }\n", + "\n", + " // 有効 IPv4 は合計桁数 4〜12 のみ\n", + " if (n < 4 || n > 12) return [];\n", + "\n", + " const res: string[] = [];\n", + " const path: string[] = new Array(4); // 再利用・固定長\n", + "\n", + " /**\n", + " * 深さ優先探索 with 枝刈り\n", + " * @param idx - 次に読むインデックス\n", + " * @param seg - 現在のセグメント番号(0..3)\n", + " */\n", + " const dfs = (idx: number, seg: number): void => {\n", + " if (seg === 4) {\n", + " if (idx === n) res.push(path.join('.'));\n", + " return;\n", + " }\n", + "\n", + " const remainSegs = 4 - seg;\n", + " const remainChars = n - idx;\n", + "\n", + " // 残文字が少なすぎる/多すぎるなら即棄却\n", + " if (remainChars < remainSegs || remainChars > remainSegs * 3) return;\n", + "\n", + " // 先頭 '0' は \"0\" のみ(leading zero 禁止)\n", + " const firstIsZero = s.charCodeAt(idx) === 48 /* '0' */;\n", + " const maxLen = firstIsZero ? 1 : 3;\n", + "\n", + " let val = 0; // セグメントの数値を逐次生成\n", + "\n", + " for (let len = 1; len <= maxLen && idx + len <= n; len++) {\n", + " // 逐次数値化:val = val*10 + digit\n", + " val = val * 10 + (s.charCodeAt(idx + len - 1) - 48);\n", + " if (val > 255) break; // 255 超過で以降は全て超過\n", + "\n", + " // 文字列化は必要最小限(len===1 は s[idx] を直接使う)\n", + " path[seg] = (len === 1) ? s[idx] : s.slice(idx, idx + len);\n", + " dfs(idx + len, seg + 1);\n", + " }\n", + " };\n", + "\n", + " dfs(0, 0);\n", + " return res;\n", + "}\n", + "\n", + "// ESM デフォルトエクスポート(Node.js v22.14.0 ESM 前提)\n", + "export default restoreIpAddresses;\n", + "\n", + "Analyze Complexity\n", + "\n", + "Runtime 0 ms\n", + "Beats 100.00%\n", + "Memory 55.10 MB\n", + "Beats 91.87%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "## 5. 制約条件(遵守状況)\n", + "\n", + "* **外部ライブラリ**:未使用(Node 標準のみ)\n", + "* **メモリ**:補助配列は固定長4・再利用、**O(1)**(出力は除く)\n", + "* **TypeScript strict mode**:前提(`\"strict\": true` を想定)\n", + "* **Pure function**:副作用なし、同一入力に対して同一結果\n", + "\n", + "---\n", + "\n", + "### TypeScript固有の最適化観点(補足)\n", + "\n", + "* **型推論 × 明示型のバランス**:外部 API との境界(関数引数/戻り値)は明示、内部は推論に任せ最小限の注釈。\n", + "* **`readonly`/イミュータブル志向**:引数は書き換えず、作業領域は**最小・再利用**(`path` のみ)。\n", + "* **実行時型ガード**:`typeof s === 'string'` と `charCodeAt` 範囲検査で**数字のみ**を担保。これにより**先頭0判定**・**255判定**が安全かつ高速に実行可能。\n", + "\n", + "> 以上で、**速度・メモリ効率・型安全性**を同時に満たす実装です。LeetCode 提出時は `restoreIpAddresses` 関数本体のみでも動作します。\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.12.4", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 00000000..f5e316db --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,13 @@ +// prettier.config.cjs +/** @type {import('prettier').Config} */ +module.exports = { + semi: true, + singleQuote: true, + trailingComma: 'all', + tabWidth: 4, + useTabs: false, + printWidth: 100, + bracketSpacing: true, + arrowParens: 'always', + endOfLine: 'lf', +}; diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index dd2b0099..00000000 --- a/prettier.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import("prettier").Config} */ -export default { - semi: true, // 文末にセミコロンを付ける - singleQuote: true, // シングルクォートを使用 - trailingComma: "all", // 複数行の配列・オブジェクトなどの末尾にカンマを付ける - tabWidth: 4, // インデント幅は2スペース - useTabs: false, // スペースでインデント - printWidth: 100, // 1行の最大文字数(超えると改行される) - bracketSpacing: true, // オブジェクトリテラルの中にスペースを入れる: { foo: bar } - arrowParens: "always", // アロー関数の引数に括弧を常に付ける - endOfLine: "lf", // 改行コードをLFに統一(Gitなどで混乱を避けるため) -};