From e1356067f9a3437d7815e606529642c087e56262 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sun, 1 Feb 2026 12:10:01 +0900 Subject: [PATCH] =?UTF-8?q?docs:=202623.=20Memoize=20II=20=E3=81=AE?= =?UTF-8?q?=E8=A7=A3=E8=AA=AC=E8=B3=87=E6=96=99=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md: 不等号の表示修正 - README_react.html: ダイアグラムの表示崩れ修正と可視化の改善 - Memoize_TS.ipynb: TypeScript実装のノートブック --- .../Claude Code Sonnet 4.5/Memoize_TS.ipynb | 190 +++++ .../Claude Code Sonnet 4.5/README.md | 243 ++++++ .../Claude Code Sonnet 4.5/README_react.html | 804 ++++++++++++++++++ 3 files changed, 1237 insertions(+) create mode 100644 JavaScript/2623. Memoize/Claude Code Sonnet 4.5/Memoize_TS.ipynb create mode 100644 JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README.md create mode 100644 JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README_react.html diff --git a/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/Memoize_TS.ipynb b/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/Memoize_TS.ipynb new file mode 100644 index 00000000..a3a4c125 --- /dev/null +++ b/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/Memoize_TS.ipynb @@ -0,0 +1,190 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9f1d722c", + "metadata": {}, + "source": [ + "## 1. 問題の分析\n", + "\n", + "**競技プログラミング視点での分析**\n", + "\n", + "この問題の本質は「引数の組み合わせをキーとしたキャッシュ(HashMap)の構築」です。引数は順序に敏感であり、`(a, b)` と `(b, a)` は異なるキーとする必要があります。キャッシュのルックアップは O(1) で、最大の懸念点はキー生成の文字列結合コストです。`Map` の文字列キーとして引数を結合すれば、全操作が O(1) に収まります。\n", + "\n", + "`fib` や `factorial` は再帰関数ですが、**LeetCode側がpasses する関数自体は再帰しない**点に注意。つまりmemoize関数が受け取る `fn` はすでに定義された関数であり、内部の再帰がmemoize貫通するかどうかは問題の設定に依存しません。Example 3で `fib(5)` の `getCallCount` が `1` であることが示されているため、**外部から見た呼び出し回数のカウント**で充分です。\n", + "\n", + "**業務開発視点での分析**\n", + "\n", + "型安全性の観点では、引数が `number[]` の可変長であることが最大の課題です。キーの生成には信頼できる区切り文字が必要で、引数そのものが区切り文字と混同されないように設計する必要があります。`Map` を使用し、キーを明確に構築することで保守性も確保できます。\n", + "\n", + "**TypeScript特有の考慮点**\n", + "\n", + "LeetCodeが提供するシグネチャ `type Fn = (...params: number[]) => number` を遵守しつつ、キャッシュの型を明確に定義する。返り関数には `getCallCount` プロパティを付与するが、LeetCode側がこれをどう扱うかは問題の構成に任せ、コアのmemoize logic だけを実装する。\n", + "\n", + "---\n", + "\n", + "## 2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 |\n", + "|---|---|---|---|---|---|---|\n", + "| **Map + 文字列キー(JSON)** | O(k) キー生成 / O(1) ルーキュップ | O(m) キャッシュエントリ数 | 低 | 高 | 高 | `JSON.stringify` は汎用だが引数がオブジェクトの場合注意要 |\n", + "| **Map + カスタム区切り文字結合** | O(k) キー生成 / O(1) ルーキュップ | O(m) | 最低 | 高 | 最高 | 引数が `number[]` なので区切り文字の衝突なし。最も軽量 |\n", + "| **ネストされたMap(Trie風)** | O(k) 各引数ごと | O(m × k) | 中 | 中 | 低 | 引数が少数で固定なら有効だが過度にコンパレクス |\n", + "\n", + "> `k` = 引数の個数、`m` = キャッシュに格納されたユニーク引数組み合わせ数\n", + "\n", + "---\n", + "\n", + "## 3. 選択したアルゴリズムと理由\n", + "\n", + "**選択したアプローチ**: Map + カスタム区切り文字結合\n", + "\n", + "**理由**:\n", + "- 引数が全て `number` であることが保証されているため、区切り文字 `,` で結合すれば衝突は発生しない(`JSON.stringify` の方が汎用だが、ここでは不要なオーバーヘッド)\n", + "- キャッシュの読み書きが O(1) で、キー生成も引数数に線形\n", + "- `Map` は挿入順を保持し、キーの存在チェックが明確で型安全\n", + "- LeetCodeのシグネチャに最も自然に収まる\n", + "\n", + "---\n", + "\n", + "## 4. 実装コード\n", + "\n", + "```typescript\n", + "// Analyze Complexity\n", + "// Runtime 264 ms\n", + "// Beats 53.64%\n", + "// Memory 96.68 MB\n", + "// Beats 15.19%\n", + "\n", + "type Fn = (...params: number[]) => number;\n", + "\n", + "function memoize(fn: Fn): Fn {\n", + " const cache = new Map();\n", + " let callCount = 0;\n", + "\n", + " const memoized: Fn = function (...args: number[]): number {\n", + " const key = args.join(\",\");\n", + "\n", + " if (cache.has(key)) {\n", + " return cache.get(key)!;\n", + " }\n", + "\n", + " callCount += 1;\n", + " const result = fn(...args);\n", + " cache.set(key, result);\n", + " return result;\n", + " };\n", + "\n", + " (memoized as any).getCallCount = (): number => callCount;\n", + "\n", + " return memoized;\n", + "}\n", + "```\n", + "\n", + "**設計の内訳**:\n", + "\n", + "`args.join(\",\")` がキー生成の核心です。引数が `[2, 2]` なら `\"2,2\"`、`[1, 2]` なら `\"1,2\"` となり、順序に敏感なキーが自然に生まれます。`cache.has(key)` で存在確認を先に行い、ヒットの場合は `fn` を一切呼び出さないことで `callCount` の精度を維持します。\n", + "\n", + "`Map` の選択は `Object` より優れる理由があります。プロトタイプ汚染のリスクがなく、キーの存在確認が `has()` で明確で、数値キーの文字列変換による暗黙の型変換も無いです。\n", + "\n", + "`(memoized as any).getCallCount` は LeetCode の判定ハネス側で使われる拡張プロパティです。型シグネチャ `Fn` に収まらないため `any` キャストが必要ですが、これはLeetCode環境の制約による妥協で、コア logic の型安全性には影響しません。\n", + "\n", + "## 問題の特定\n", + "\n", + "Runtime 264ms・Memory 96.68MB という結果から、主な瓶目標が2つあります。\n", + "\n", + "1. **メモリ 96.68MB(15.19%)** — これが最大の課題。`Map` と文字列キー生成が膨らんでいる。\n", + "2. **Runtime 264ms(53.64%)** — キー生成の文字列結合・`join()` のコストが累積している。\n", + "\n", + "`join(\",\")` は毎呼び出しで新しい文字列オブジェクトを生成し、`Map` もその文字列キーを保持し続けます。引数が `number[]` で制約が明確なのに、文字列という「重い抽象」を使っている点が根本的な損失です。\n", + "\n", + "---\n", + "\n", + "## アプローチ比較(改善案)\n", + "\n", + "| アプローチ | Runtime | Memory | 説明 |\n", + "|---|---|---|---|\n", + "| 現行: `Map` + `join` | O(k) キー生成 | O(m × k) 文字列保持 | 文字列オブジェクト生成・保持が重い |\n", + "| **案A: 数値キー直接エンコード** | O(1) キー計算 | O(m) 数値のみ | `sum` の引数を1つの数値に圧縮 |\n", + "| **案B: ネスト `Map`(2階層)** | O(1) ルーキュップ | O(m) ポインタのみ | 文字列キーを全廃、数値キーで直接インデックス |\n", + "\n", + "引数の制約は以下の通りです。\n", + "- `sum`: `0 <= a, b <= 10^5` → 引数は2つの非負整数\n", + "- `fib`/`factorial`: `1 <= n <= 10` → 引数は1つの整数\n", + "\n", + "これが鍵です。`sum` の引数は最大 `10^5` なので、`a * (10^5 + 1) + b` で**1つの整数に圧縮**できます。これにより文字列キーは完全に廃除されます。\n", + "\n", + "---\n", + "\n", + "## 改善コード\n", + "\n", + "```typescript\n", + "// Analyze Complexity\n", + "// Runtime 235 ms\n", + "// Beats 95.58%\n", + "// Memory 95.88 MB\n", + "// Beats 57.27%\n", + "\n", + "type Fn = (...params: number[]) => number;\n", + "\n", + "function memoize(fn: Fn): Fn {\n", + " // sum: 引数2つ(a, b) → a * 100001 + b で一意な整数キーに圧縮\n", + " // fib/factorial: 引数1つ(n) → nそのもの\n", + " // 両方対応するため、ネスト Map を使用しない。\n", + " // 引数数で分岐し、数値キーのみで Map を構築する。\n", + " const cache = new Map();\n", + " let callCount = 0;\n", + "\n", + " const memoized: Fn = function (...args: number[]): number {\n", + " // 引数が1つなら n そのもの、2つなら圧縮キー\n", + " const key = args.length === 1\n", + " ? args[0]\n", + " : args[0] * 100001 + args[1];\n", + "\n", + " if (cache.has(key)) {\n", + " return cache.get(key)!;\n", + " }\n", + "\n", + " callCount += 1;\n", + " const result = fn(...args);\n", + " cache.set(key, result);\n", + " return result;\n", + " };\n", + "\n", + " (memoized as any).getCallCount = (): number => callCount;\n", + "\n", + " return memoized;\n", + "}\n", + "```\n", + "\n", + "---\n", + "\n", + "## 改善の詳細\n", + "\n", + "**キー圧縮の正当性の確認です。**\n", + "\n", + "`a * 100001 + b` で衝突しないことを検証します。異なる `(a1, b1)` と `(a2, b2)` があって同じキーを生成したとすると:\n", + "\n", + "```\n", + "a1 * 100001 + b1 === a2 * 100001 + b2\n", + "→ (a1 - a2) * 100001 === b2 - b1\n", + "```\n", + "\n", + "`b` の範囲が `0 ~ 10^5` なので `|b2 - b1| <= 10^5 < 100001` です。よって左辺が `100001` の倍数になるためには `a1 === a2` が必要で、それは `b1 === b2` を意味します。つまり衝突は不可能に proven されます。\n", + "\n", + "**何が変わったかの整理です。**\n", + "\n", + "現行コードでは、呼び出しのたびに `args.join(\",\")` が新しい文字列オブジェクトを確保し、その文字列が `Map` のキーとして永続保持されました。改善版では引数を一つの数値に圧縮し、`Map` で管理します。文字列オブジェクトの生成がゼロに、キーの保持も数値(8バイト)に圧縮されます。これがメモリの大幅削減とRuntimeの改善の両方に直結します。\n", + "\n", + "**1つの引数の場合**(`fib`/`factorial`)では `n` は最大 `10` なので、キーをそのまま使うことで圧縮演算自体も廃除されます。" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README.md b/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README.md new file mode 100644 index 00000000..5a370d9e --- /dev/null +++ b/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README.md @@ -0,0 +1,243 @@ +# Memoize II - 引数の順序を保持したキャッシュ関数の構築 + +--- + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点 TL;DR](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [TypeScript 実装](#impl) +- [最適化履歴と改善の根拠](#optimizations) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +与えられた関数 `fn` に対し、**同じ引数の組み合わせに対して `fn` を再度呼び出さない** メモイズ版を返す。引数の順序は意味を持ち、`(a, b)` と `(b, a)` は異なるキーとする。 + +対象関数は以下の3種に限定される。 + +| 関数 | 引数 | 制約 | +| ----------- | ------------ | ----------------- | +| `sum` | `a, b` (2つ) | `0 ≤ a, b ≤ 10^5` | +| `fib` | `n` (1つ) | `1 ≤ n ≤ 10` | +| `factorial` | `n` (1つ) | `1 ≤ n ≤ 10` | + +**キャッシュのヒット判定には引数の順序が重要**。外部から観測される呼び出し回数は `getCallCount()` で取得される。 + +--- + +

アルゴリズム要点 TL;DR

+ +- **戦略**: 引数を単一の整数キーに圧縮し、`Map` で O(1) キャッシュ +- **キー設計**: + - 引数1つの場合: キー = `n` そのもの + - 引数2つの場合: キー = `a * 100001 + b`(圧縮キー) +- **データ構造**: `Map`(キー: 圧縮整数、値: キャッシュ結果) +- **計算量**: Time O(1) per call / Space O(m) — `m` はキャッシュエントリ数 +- **メモリ設計**: 文字列キーを完全に廃除し、数値キーのみで構築 + +--- + +

図解

+ +### フローチャート — 呼び出し時の制御フロー + +```mermaid +flowchart TD + Start[Receive args] --> Encode[Compute numeric key] + Encode --> Hit{Cache hit?} + Hit -- Yes --> Ret[Return cached value] + Hit -- No --> Inc[Increment callCount] + Inc --> Exec[Execute fn with args] + Exec --> Store[Store result in cache] + Store --> Ret +``` + +> `args` が到着した瞬間に数値キーに圧縮し、`Map` を1回だけ参照する。ヒットなら関数実行をスキップ。 + +--- + +### データフロー図 — キー圧縮の仕組み + +```mermaid +graph LR + subgraph Input + A[args: number array] --> B{args.length} + end + subgraph KeyEncode + B -- 1 --> C[key = args 0] + B -- 2 --> D[key = args 0 * 100001 + args 1] + end + subgraph Cache + C --> E[Map lookup] + D --> E + E --> F{Hit or Miss} + end + subgraph Output + F -- Hit --> G[Return cached] + F -- Miss --> H[Call fn, store, return] + end +``` + +> 引数の長さによって分岐し、いずれも数値キーとして `Map` に到達する。文字列オブジェクトの生成は発生しない。 + +--- + +

正しさのスケッチ

+ +### 1. キー圧縮の衝突不可性(不変条件) + +異なる引数ペア `(a1, b1) ≠ (a2, b2)` が同じキーを生成しないことを示す。 + +``` +a1 * 100001 + b1 = a2 * 100001 + b2 +→ (a1 - a2) * 100001 = b2 - b1 +``` + +`b` の範囲が `0 ~ 10^5` なので `|b2 - b1| ≤ 10^5 < 100001`。 +左辺は `100001` の整数倍にならないため、`a1 = a2` かつ `b1 = b2` のみが成り立つ。 +**衝突は定理的に不可能。** + +### 2. 順序の正確性(網羅性) + +`(3, 2)` と `(2, 3)` のキーはそれぞれ `3 * 100001 + 2 = 300005` と `2 * 100001 + 3 = 200005` となり異なる。キャッシュの混同は発生しない。 + +### 3. 基底条件 + +キャッシュが空の初期状態では必ず `cache.has(key)` が `false` となり、`fn` が実行される。 + +### 4. 終了性 + +キャッシュのルーキュップと数値演算は定常時間で終了する。無限ループは発生しない。 + +--- + +

計算量

+ +| 操作 | 時間計算量 | 空間計算量 | 備考 | +| ------------------ | -------------- | ---------- | ------------------------------ | +| キー計算 | O(1) | O(1) | 乗算・加算のみ | +| `Map` ルーキュップ | O(1) 平均 | — | ハッシュテーブル | +| キャッシュ保持 | — | O(m) | `m` = ユニーク引数組み合わせ数 | +| 呼び出し全体 | O(1) amortized | O(m) | `fn` の実行コストは含まない | + +### 現行実装 vs 改善前の比較 + +| 指標 | 改善前(文字列キー) | 現行(数値キー) | +| --------------------------- | ------------------------------------------ | ------------------------- | +| キー型 | `string` | `number` | +| キー生成コスト | O(k) — `join` で新規文字列オブジェクト生成 | O(1) — 乗算・加算のみ | +| キャッシュ1エントリのメモリ | 文字列キー + 数値値 | 数値キー + 数値値(最小) | +| `Map` 型 | `Map` | `Map` | + +--- + +

TypeScript 実装

+ +```typescript +type Fn = (...params: number[]) => number; + +function memoize(fn: Fn): Fn { + // キャッシュ: 数値キー → 計算結果 + const cache = new Map(); + + // 外部から観測される実際の関数呼び出し回数 + let callCount = 0; + + const memoized: Fn = function (...args: number[]): number { + // キー圧縮: + // 引数1つ → n そのもの (fib, factorial) + // 引数2つ → a * 100001 + b (sum) + // 100001 = 10^5 + 1 で、a と b の組み合わせが一意に対応 + const key = args.length === 1 ? args[0] : args[0] * 100001 + args[1]; + + // キャッシュヒット: fn を呼び出せず結果を返す + if (cache.has(key)) { + return cache.get(key)!; + } + + // キャッシュミス: fn を実行し結果を保存 + callCount += 1; + const result = fn(...args); + cache.set(key, result); + return result; + }; + + // LeetCode の判定ハネス側から呼ばれる拡張プロパティ + // Fn 型には収まらないため any キャスト(コア logic には影響なし) + (memoized as any).getCallCount = (): number => callCount; + + return memoized; +} +``` + +--- + +

最適化履歴と改善の根拠

+ +### 初期実装(文字列キー)の問題点 + +```typescript +// ❌ 初期実装 +const key = args.join(','); // 毎呼び出しで新規文字列オブジェクト生成 +const cache = new Map(); // キーの永続保持がメモリ圧迫 +``` + +| 問題 | 影響 | +| ------------------------------ | --------------------------- | +| `join(",")` が毎回文字列を確保 | GC圧力の増加、Runtime 264ms | +| 文字列キーの永続保持 | Memory 96.68MB(15.19%) | + +### 現行実装への移行の理由 + +制約 `0 ≤ a, b ≤ 10^5` を活用し、2つの引数を**1つの整数**に圧縮する。これにより: + +- 文字列オブジェクトの生成がゼロに +- キャッシュ1エントリのメモリが最小化(数値8バイト × 2 のみ) +- キー計算が単純な乗算・加算に軽量化 + +### なぜ `100001` か + +`b` の最大値が `10^5` なので、異なる `a` で生成されるキー範囲が重ならないためには乗数が `10^5 + 1 = 100001` 以上であること。`100001` がその最小値として選ばれる。 + +--- + +

エッジケースと検証観点

+ +| エッジケース | 期待動作 | 検証観点 | +| -------------------------- | ------------------------------------- | ----------------------------------------------- | +| `sum(0, 0)` | キー `0` で正しくキャッシュ | ゼロが引数の場合の正確性 | +| `sum(0, 1)` vs `sum(1, 0)` | キー `1` と `100001` で異なるエントリ | 順序の区別 | +| `sum(100000, 100000)` | キー `10000200000` で正確に動作 | 最大引数での整数オーバーフロー確認(JS は安全) | +| `fib(1)` | キャッシュミスで `1` を返す | 基底条件の正確性 | +| `factorial(1)` | キャッシュミスで `1` を返す | 基底条件の正確性 | +| 同じ引数の連続呼び出し | 2回目以降は `callCount` を増加しない | キャッシュヒットの正確性 | +| `getCallCount` の初期値 | `0` | 呼び出し前の状態 | + +**整数オーバーフローの確認**: 最大キー `100000 * 100001 + 100000 = 10_000_200_000`。JavaScript の `Number.MAX_SAFE_INTEGER` は `2^53 - 1 ≈ 9 × 10^15` なので、安全範囲の中に収まる。 + +--- + +

FAQ

+ +**Q: なぜ `JSON.stringify` ではなく数値キーを選んだのか?** + +`JSON.stringify` も正確だが、毎呼び出しで文字列オブジェクトを生成するため、メモリと実行時間の両方で損失がある。制約が明確に数値に限定されているため、数値キーが最適。 + +**Q: なぜ `(memoized as any)` キャスト が必要なのか?** + +LeetCode側の判定ハネスが `getCallCount()` プロパティを期待するが、`type Fn` の定義にはこのプロパティが含まれない。`any` キャストは環境の制約による妥協であり、キャッシュロジック自体の型安全性には影響しない。 + +**Q: `fib` や `factorial` の内部再帰もメモイズされるのか?** + +この実装はメモイズを外側のラッパーで行う。`fib` や `factorial` の内部再帰がこのラッパーを通過するかは、LeetCode側が渡す関数の定義に依存する。Example 3で `fib(5)` の `getCallCount` が `1` であることが確認されているため、外部から見た呼び出し回数のカウントで充分。 + +**Q: `Map` の代わりに配列を使えないか?** + +`sum` のキーが最大 `10^10` オーダーなので、配列インデックスとして使うと巨大な疎配列になりメモリが膨らむ。`Map` がこのケースの最適解。 diff --git a/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README_react.html b/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README_react.html new file mode 100644 index 00000000..b810fc09 --- /dev/null +++ b/JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README_react.html @@ -0,0 +1,804 @@ + + + + + + LeetCode 2623: Memoize II - 引数の順序を保持したキャッシュ関数 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

問題の説明

+

+ 与えられた関数 fn に対し、同じ引数の組み合わせに対して再度呼び出さないメモイズ版を返します。引数の順序は意味を持ち、(a, b)(b, a) は異なるキーとして扱います。 +

+
+ +
+

対象関数

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
関数引数制約
suma, b (2つ)0 ≤ a, b ≤ 105
fibn (1つ)1 ≤ n ≤ 10
factorialn (1つ)1 ≤ n ≤ 10
+
+
+ +
+

入出力例

+
+
Input: fnName = "sum", actions = ["call","call","getCallCount","call","getCallCount"]
+       values = [[2,2],[2,2],[],[1,2],[]]
+Output: [4,4,1,3,2]
+
+Explanation:
+memoizedSum(2, 2); // returns 4, sum() が呼ばれる(初回)
+memoizedSum(2, 2); // returns 4, sum() は呼ばれない(キャッシュヒット)
+getCallCount();     // returns 1
+memoizedSum(1, 2); // returns 3, sum() が呼ばれる(新しい引数)
+getCallCount();     // returns 2
+
+
+ +
+

戦略

+
    +
  • + + キー圧縮: 引数を単一の整数キーに変換し、Map<number, number> で O(1) キャッシュ +
  • +
  • + + 引数1つ: キー = n そのもの(fib, factorial) +
  • +
  • + + 引数2つ: キー = a × 100001 + b(sum)— 衝突不可能な圧縮 +
  • +
  • + + 順序保持: (3, 2) のキーは 300005、(2, 3) のキーは 200005 で異なる +
  • +
+
+ +
+

主要ポイント

+
    +
  • 時間計算量: O(1) per call(キー計算とMap操作)
  • +
  • 空間計算量: O(m)(m = ユニーク引数組み合わせ数)
  • +
  • 最適化手法: 文字列キーを完全に廃除し、数値キーのみで構築
  • +
+
+
+ + +
+

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

+
+
+ + +
+

+ TypeScript実装 +

+
type Fn = (...params: number[]) => number;
+
+function memoize(fn: Fn): Fn {
+    // キャッシュ: 数値キー → 計算結果
+    const cache = new Map();
+
+    // 外部から観測される実際の関数呼び出し回数
+    let callCount = 0;
+
+    const memoized: Fn = function (...args: number[]): number {
+        // キー圧縮:
+        //   引数1つ → n そのもの (fib, factorial)
+        //   引数2つ → a * 100001 + b (sum)
+        // 100001 = 10^5 + 1 で、a と b の組み合わせが一意に対応
+        const key = args.length === 1 ? args[0] : args[0] * 100001 + args[1];
+
+        // キャッシュヒット: fn を呼び出さず結果を返す
+        if (cache.has(key)) {
+            return cache.get(key)!;
+        }
+
+        // キャッシュミス: fn を実行し結果を保存
+        callCount += 1;
+        const result = fn(...args);
+        cache.set(key, result);
+        return result;
+    };
+
+    // LeetCode の判定ハーネス側から呼ばれる拡張プロパティ
+    // Fn 型には収まらないため any キャスト(コア logic には影響なし)
+    (memoized as any).getCallCount = (): number => callCount;
+
+    return memoized;
+}
+
+ + +
+

+ フローチャート +

+
+ + + + + + + + + + + + + + + + + 引数受信 + + + + + + + + + 数値キーを計算 + + + args.length === 1 ? args[0] + + + : args[0] * 100001 + args[1] + + + + + + + + + cache.has(key)? + + + キャッシュヒット? + + + + + はい + + + + + キャッシュから返す + + + cache.get(key) + + + + + いいえ + + + + + callCount += 1 + + + + + + + + + result = fn(...args) + + + + + + + + + cache.set(key, result) + + + + + + + + + + 結果を返す + + +
+ +

+ フローの説明:
+ 1. 引数受信: 関数が引数を受け取る
+ 2. 数値キーを計算: 引数の個数に応じてキーを生成(1つなら n、2つなら a × 100001 + b)
+ 3. キャッシュヒット判定: Map にキーが存在するか確認
+ 4. キャッシュヒット: 既存の結果を返す(fn を呼び出さない)
+ 5. キャッシュミス: callCount を増加し、fn を実行して結果をキャッシュに保存
+ 6. 結果を返す: 計算またはキャッシュから取得した結果を返す +

+
+ + +
+

+ 計算量分析 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作時間計算量空間計算量備考
キー計算O(1)O(1)乗算・加算のみ
Map ルックアップO(1) 平均ハッシュテーブル
キャッシュ保持O(m)m = ユニーク引数組み合わせ数
呼び出し全体O(1) amortizedO(m)fn の実行コストは含まない
+
+ +
+

最適化の比較

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指標改善前(文字列キー)現行(数値キー)
キー型stringnumber
キー生成コストO(k) — join で新規文字列生成O(1) — 乗算・加算のみ
メモリ効率文字列キー + 数値値数値キー + 数値値(最小)
Map 型Map<string, number>Map<number, number>
+
+
+ +
+

キー圧縮の正しさ

+

+ なぜ 100001 か? b の最大値が 105 なので、異なる a で生成されるキー範囲が重ならないためには乗数が 105 + 1 = 100001 以上である必要があります。 +

+

+ 衝突不可能性の証明: 異なる引数ペア (a₁, b₁) ≠ (a₂, b₂) が同じキーを生成しないことを示します。 +

+
+ a₁ × 100001 + b₁ = a₂ × 100001 + b₂
+ → (a₁ - a₂) × 100001 = b₂ - b₁
+
+ b の範囲が 0 ~ 10⁵ なので |b₂ - b₁| ≤ 10⁵ < 100001
+ 左辺は 100001 の整数倍にならないため、a₁ = a₂ かつ b₁ = b₂ のみが成り立つ。
+ ∴ 衝突は定理的に不可能 +
+
+
+
+ + + + + + + +