From 627265c3487d0c13092d301faea848ccfea2af57 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sat, 4 Oct 2025 18:46:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?leetcode=203.=20Longest=20Substring=20Witho?= =?UTF-8?q?ut=20Repeating=20Characters=20ASCII=20=E6=9C=80=E9=81=A9?= =?UTF-8?q?=E5=8C=96=EF=BC=8B=E5=BF=85=E8=A6=81=E6=99=82=E3=81=AE=E3=81=BF?= =?UTF-8?q?=E6=98=87=E6=A0=BC=20=E3=82=B9=E3=83=A9=E3=82=A4=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=E3=82=A6=E3=82=A3=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=82=A6=EF=BC=8B=20=E7=9B=B4=E8=BF=91=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E3=83=86=E3=83=BC=E3=83=96=E3=83=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Claude/README.html | 2016 +++++++++++++++++ .../Claude/README.md | 380 ++++ ...gestSubstringWithoutRepeatingCharacters.js | 281 +++ ...gestSubstringWithoutRepeatingCharacters.py | 286 +++ ...gestSubstringWithoutRepeatingCharacters.ts | 121 + .../GPT/README.md | 285 +++ 6 files changed, 3369 insertions(+) create mode 100644 Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.html create mode 100644 Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.md create mode 100644 Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.js create mode 100644 Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.py create mode 100644 Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.ts create mode 100644 Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/README.md diff --git a/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.html b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.html new file mode 100644 index 00000000..e31c4b61 --- /dev/null +++ b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.html @@ -0,0 +1,2016 @@ + + + + + + + Longest Substring Without Repeating Characters - + スライディングウィンドウ+高速位置管理 + + + + + + + + + + + + + + + + + + + + + +
+
+

Longest Substring Without Repeating Characters

+

スライディングウィンドウ+高速位置管理によるO(n)解法

+
+
+ + + +
+
+
+

概要

+
+

+ 問題 +

+

+ 文字列 + s + が与えられたとき、重複文字を含まない最長の連続部分文字列の長さを求めます。 +

+ +

+ 例 +

+
    +
  • + s = "abcabcbb" → 出力: 3("abc") +
  • +
  • s = "bbbbb" → 出力: 1("b")
  • +
  • s = "pwwkew" → 出力: 3("wke")
  • +
+ +

+ 制約 +

+
    +
  • 入力長: 0 ≤ n ≤ 5×10⁴
  • +
  • 文字種: 英字・数字・記号・空白(ASCII + 非ASCII混在可能)
  • +
  • + 部分文字列(substring)であり、部分列(subsequence)ではない +
  • +
+ +

+ アルゴリズム要点 +

+
    +
  • + スライディングウィンドウ: + 右端を進めつつ、左端を必要に応じて前進 +
  • +
  • + ASCII高速化: + array('I', 128) で固定・高速アクセス +
  • +
  • + 非ASCII対応: + dict でメモリ削減(BMP 65536配列を回避) +
  • +
  • Time: O(n) — 各文字を高々1回走査
  • +
  • + Space: O(1) + 相当(ASCII固定128、非ASCIIは出現数に比例) +
  • +
+
+
+
+ +
+
+

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

+
+
+
+ +
+
+

Python実装(LeetCode形式)

+
+

+ 以下は、メモリ削減版の実装です。ASCIIは固定配列、非ASCIIは辞書で管理します。 +

+
from __future__ import annotations
+
+from array import array
+from typing import Dict, Final
+
+
+class Solution:
+    """
+    Longest Substring Without Repeating Characters
+
+    メモリ削減版:
+    - ASCII (0..127) は array('I', 128) の軽量表(未出現=0, 出現は index+1)
+    - 非ASCII (>=128) は dict に格納(BMP 65536表は使わない)
+      → 65536 要素配列(約256KB) の確保を完全に回避してピークメモリを下げる
+    """
+
+    _MAX_LEN: Final[int] = 5 * 10**4
+
+    def lengthOfLongestSubstring(self, s: str) -> int:
+        """
+        Args:
+            s: 入力文字列
+
+        Returns:
+            重複のない最長連続部分文字列の長さ
+
+        Raises:
+            TypeError: s が str でない場合
+            ValueError: 入力長が仕様上限を超える場合
+
+        Complexity:
+            Time: O(n)
+            Space: O(1) 相当(ASCIIは固定128、非ASCIIは出現数に比例)
+        """
+        # 入力検証
+        if not isinstance(s, str):
+            raise TypeError("Input must be a string")
+        n: int = len(s)
+        if n > self._MAX_LEN:
+            raise ValueError("Input length exceeds allowed maximum")
+
+        # 基底条件: 空文字列
+        if n == 0:
+            return 0
+
+        # ASCII 用(0..127)だけ固定確保:極小&高速
+        last_ascii: array = array("I", [0]) * 128
+        # 非ASCII は dict にのみ格納(BMP 65536表は作らない)
+        last_other: Dict[int, int] = {}
+
+        left: int = 0  # ウィンドウ左端
+        best: int = 0  # 最大長
+
+        for i, ch in enumerate(s):
+            code: int = ord(ch)
+
+            # 分岐: ASCIIか非ASCIIか
+            if code < 128:
+                prev: int = last_ascii[code]
+                # 重複検出: 前回出現がウィンドウ内なら左端を前進
+                if prev > left:
+                    left = prev
+                # 現在位置を記録(1-indexed: 0 は未出現)
+                last_ascii[code] = i + 1
+            else:
+                prev = last_other.get(code, 0)
+                if prev > left:
+                    left = prev
+                last_other[code] = i + 1
+
+            # 現在ウィンドウの長さを計算
+            curr_len: int = i - left + 1
+            if curr_len > best:
+                best = curr_len
+
+        return best
+
+
+
+ +
+
+

視覚的図解

+
+

+ アルゴリズムフローチャート +

+ + + + + + + + + + + + + + + + + + 開始 + + + + + + + 入力は + + + 有効か? + + + + + + No + + + + エラー + + + + + + Yes + + + + n == 0? + + + + + + Yes + + + + 0を返す + + + + + + No + + + + 初期化 + + + left=0, best=0 + + + + + + + 各文字 i, ch + + + を処理 + + + + + + + code < 128? + + + + + + Yes + + + + 配列から取得 + + + last_ascii[code] + + + + + + No + + + + 辞書から取得 + + + last_other.get() + + + + + + + + + + + prev > left? + + + + + + Yes + + + + left = prev + + + + + + No + + + + + 位置を記録 + + + 長さ計算、best更新 + + + + + + 次の文字へ + + + + + + 全文字処理完了 + + + + bestを返す + + + +

+ フローの説明:
+ 1. 入力検証(型チェック・長さチェック)→ エラーなら例外を発生
+ 2. 空文字列なら0を返して終了
+ 3. 初期化:left=0(ウィンドウ左端)、best=0(最大長)
+ 4. 各文字について:
+  ・ASCII(code<128)なら配列から、非ASCIIなら辞書から前回出現位置を取得
+  ・前回位置がウィンドウ内(prev>left)なら、leftを前進して重複を排除
+  ・現在位置を記録し、ウィンドウ長を計算してbestを更新
+ 5. 全文字を処理したら、bestを返して終了 +

+
+
+
+
+
+

計算量

+
+

+ 時間計算量: O(n) +

+
    +
  • 各文字を高々1回走査
  • +
  • ASCIIは配列で O(1) アクセス、非ASCIIは辞書で平均 O(1)
  • +
  • 左端 left は単調増加(最大 n まで)
  • +
+ +

+ 空間計算量: O(1) 相当 +

+
    +
  • ASCII部分: array('I', 128) → 512バイト固定
  • +
  • + 非ASCII部分: + dict は出現種類数に比例(実際は小規模) +
  • +
+ +

+ 実装比較 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
実装TimeSpace (ASCII)Space (非ASCII)メモリ順位
修正前(BMP 65536配列)O(n)512B約256KB(BMP表)中程度
修正後(dict)O(n)512B出現種類数に比例改善
+
+
+
+
+ + + + + + + + + + + + + + diff --git a/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.md b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.md new file mode 100644 index 00000000..ab56c8e4 --- /dev/null +++ b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.md @@ -0,0 +1,380 @@ +# Longest Substring Without Repeating Characters - スライディングウィンドウ+高速位置管理 + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython 最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +**問題**: 文字列 `s` が与えられたとき、**重複文字を含まない最長の連続部分文字列**の長さを求める。 + +**要件**: + +- 入力長 `n`: `0 ≤ n ≤ 5×10⁴` +- 文字種: 英字・数字・記号・空白(ASCII + 非 ASCII 混在可能) +- **部分文字列**(substring)であり、部分列(subsequence)ではない → 連続性が必須 + +**制約**: + +- Time: **O(n)** を達成(各文字を高々 2 回走査) +- Space: **O(min(n, Σ))** 相当(文字集合サイズ Σ に比例、ASCII なら固定 128) + +--- + +

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

+ +- **戦略**: スライディングウィンドウ(右端 `i` を進めつつ、左端 `left` を必要に応じて前進) +- **データ構造**: + - **ASCII(0..127)**: `array('I', 128)` で固定・高速(未出現=0、出現は `index+1`) + - **非 ASCII(≥128)**: `dict[int, int]` で管理(BMP 65536 配列を避けてメモリ削減) +- **計算量**: + - Time: **O(n)** — 各文字を高々 1 回ずつ走査+定数時間の位置更新 + - Space: **O(1)** 相当(ASCII は固定 128、非 ASCII は出現数に比例) +- **メモリ戦略**: 以前の BMP 65536 配列(約 256KB)を撤廃し、辞書に切り替えることで**ピークメモリを大幅削減** + +--- + +

図解

+ +### フローチャート(処理の流れ) + +```mermaid +flowchart TD + Start[Start solve] --> Validate{Input valid} + Validate -- No --> Raise[Raise error] + Validate -- Yes --> Empty{n == 0} + Empty -- Yes --> RetZero[Return 0] + Empty -- No --> Init[Initialize left=0, best=0, last_ascii, last_other] + Init --> Loop{For each i,ch in s} + Loop -- No more --> Done[Return best] + Loop -- Yes --> CheckASCII{code < 128} + CheckASCII -- Yes --> GetASCII[prev = last_ascii code] + CheckASCII -- No --> GetDict[prev = last_other.get code, 0] + GetASCII --> CheckPrev{prev > left} + GetDict --> CheckPrev + CheckPrev -- Yes --> UpdateLeft[left = prev] + CheckPrev -- No --> KeepLeft[Keep left] + UpdateLeft --> StorePos[Store i+1 in last_ascii or last_other] + KeepLeft --> StorePos + StorePos --> CalcLen[curr_len = i - left + 1] + CalcLen --> UpdateBest{curr_len > best} + UpdateBest -- Yes --> SetBest[best = curr_len] + UpdateBest -- No --> KeepBest[Keep best] + SetBest --> Loop + KeepBest --> Loop + Done --> End[End] +``` + +**説明**: + +1. 入力検証(型・長さ)を行い、空文字列なら即座に `0` を返す +2. 各文字について、ASCII なら固定配列、非 ASCII なら辞書から前回出現位置を取得 +3. 前回位置が現在ウィンドウ内(`prev > left`)なら左端を前進 +4. 現在位置を記録し、ウィンドウ長を計算して最大値を更新 +5. 全文字を処理後、最大長を返す + +### データフロー図 + +```mermaid +graph LR + subgraph Input_phase + A[Input string s] --> B[Validate type and length] + B --> C[Check n == 0] + end + subgraph Init_phase + C --> D[Allocate last_ascii array 128] + C --> E[Initialize last_other dict] + end + subgraph Core_loop + D --> F[For each char i, ch] + E --> F + F --> G[Compute code = ord ch] + G --> H{code < 128} + H -- Yes --> I[Access last_ascii code] + H -- No --> J[Access last_other code] + I --> K[Update left if needed] + J --> K + K --> L[Store i+1 in last_ascii or last_other] + L --> M[Calculate curr_len] + M --> N[Update best] + N --> F + end + F --> O[Return best] +``` + +**説明**: + +- **Input_phase**: 入力の型チェック・長さ上限チェック・空文字列の早期リターン +- **Init_phase**: ASCII 用固定配列(128 要素)と非 ASCII 用辞書を初期化 +- **Core_loop**: 各文字について、コード値で分岐して ASCII 配列か辞書を参照し、ウィンドウを調整しながら最大長を更新 + +--- + +

正しさのスケッチ

+ +### 不変条件 + +**ウィンドウ `[left, i]` 内には重複文字が存在しない** + +- 各文字 `ch` について、その**前回出現位置 `prev`** を管理 +- `prev > left` なら、`ch` が既にウィンドウ内に存在 → `left = prev` として左端を前進し、重複を排除 +- `prev ≤ left` なら、`ch` はウィンドウ外またはまだ未出現 → ウィンドウを拡大可能 + +### 網羅性 + +- 右端 `i` を `0` から `n-1` まで全て走査 → すべての部分文字列の終端を試行 +- 各 `i` で左端 `left` を適切に調整 → その終端での最長ウィンドウを常に維持 +- よって、**すべての重複なし部分文字列の最大長を見逃さない** + +### 基底条件 + +- `n == 0` → `best = 0`(空文字列) +- `n == 1` → ループ 1 回で `best = 1` + +### 終了性 + +- 外側ループは `i` を `0..n-1` まで増加 → 有限ステップで終了 +- 各ステップは定数時間(配列または辞書への単一アクセス) + +--- + +

計算量

+ +### 時間計算量: **O(n)** + +- 各文字 `ch` について: + - ASCII なら `last_ascii[code]` の **O(1)** アクセス + - 非 ASCII なら `last_other.get(code, 0)` の **平均 O(1)** アクセス +- `left` は単調増加(最大 `n` まで) → 全体で高々 `2n` ステップ +- よって **線形時間** を達成 + +### 空間計算量: **O(min(n, Σ))** + +- **ASCII 部分**: `array('I', 128)` → **512 バイト固定**(4 バイト × 128) +- **非 ASCII 部分**: `dict` は出現した非 ASCII 文字数に比例(最大 `n` だが実際は出現種類数) +- トータル: **O(1) 相当**(多くの入力では ASCII 主体で辞書は小規模) + +### 比較表(修正前後) + +| 実装 | Time | Space(ASCII) | Space(非 ASCII 混在) | メモリ順位 | +| ---------------------------- | ---- | -------------- | ---------------------- | ---------- | +| **修正前**(BMP 65536 配列) | O(n) | 512B | 約 256KB(BMP 表) | 中程度 | +| **修正後**(dict) | O(n) | 512B | 出現種類数に比例 | **改善** | + +--- + +

Python 実装

+ +```python +from __future__ import annotations + +from array import array +from typing import Dict, Final + + +class Solution: + """ + Longest Substring Without Repeating Characters + + メモリ削減版: + - ASCII (0..127) は array('I', 128) の軽量表(未出現=0, 出現は index+1) + - 非ASCII (>=128) は dict に格納(BMP 65536表は使わない) + → 65536 要素配列(約256KB) の確保を完全に回避してピークメモリを下げる + """ + + _MAX_LEN: Final[int] = 5 * 10**4 + + def lengthOfLongestSubstring(self, s: str) -> int: + """ + Args: + s: 入力文字列 + + Returns: + 重複のない最長連続部分文字列の長さ + + Raises: + TypeError: s が str でない場合 + ValueError: 入力長が仕様上限を超える場合 + + Complexity: + Time: O(n) + Space: O(1) 相当(ASCIIは固定128、非ASCIIは出現数に比例) + """ + # 入力検証 + if not isinstance(s, str): + raise TypeError("Input must be a string") + n: int = len(s) + if n > self._MAX_LEN: + raise ValueError("Input length exceeds allowed maximum") + + # 基底条件: 空文字列 + if n == 0: + return 0 + + # ASCII 用(0..127)だけ固定確保:極小&高速 + last_ascii: array = array("I", [0]) * 128 + # 非ASCII は dict にのみ格納(BMP 65536表は作らない) + last_other: Dict[int, int] = {} + + left: int = 0 # ウィンドウ左端 + best: int = 0 # 最大長 + + for i, ch in enumerate(s): + code: int = ord(ch) + + # 分岐: ASCIIか非ASCIIか + if code < 128: + prev: int = last_ascii[code] + # 重複検出: 前回出現がウィンドウ内なら左端を前進 + if prev > left: + left = prev + # 現在位置を記録(1-indexed: 0 は未出現) + last_ascii[code] = i + 1 + else: + prev = last_other.get(code, 0) + if prev > left: + left = prev + last_other[code] = i + 1 + + # 現在ウィンドウの長さを計算 + curr_len: int = i - left + 1 + if curr_len > best: + best = curr_len + + return best +``` + +### 補足 + +- **`array('I', [0]) * 128`**: 符号なし整数(4 バイト)× 128 = 512 バイトの固定配列 +- **`last_other: Dict[int, int]`**: 非 ASCII 文字のコードポイント → 出現位置(1-indexed) +- **1-indexed 記録**: `0` を「未出現」の番兵として利用し、`prev > left` 判定を単純化 + +--- + +

CPython最適化ポイント

+ +### 1. **固定配列 `array('I')` で ASCII 高速化** + +- `list` や `dict` より**メモリ効率**と**キャッシュ効率**が高い +- ASCII は 128 要素固定なので**オーバーヘッドが極小** + +### 2. **辞書は非 ASCII 専用(BMP 65536 配列の回避)** + +- 以前の実装では非 ASCII 出現時に 65536 要素配列(約 256KB)を確保 +- 修正版では **`dict` にフォールバック** → 出現種類数に比例する軽量メモリ +- **ピークメモリ削減** により、LeetCode のメモリ順位(Beats%)が改善しやすい + +### 3. **属性アクセス削減** + +- ループ内で `last_ascii[code]` を直接参照(メソッド呼び出しなし) +- `last_other.get(code, 0)` は 1 回のみ(再取得しない) + +### 4. **早期リターン** + +- `n == 0` で即座に `0` を返し、不要なループを回避 + +### 5. **型注釈による静的解析** + +- pylance での型チェックを通過 → 実行時エラーを事前検出 +- `Final` でクラス定数を明示 → 再代入防止 + +--- + +

エッジケースと検証観点

+ +| ケース | 入力例 | 期待出力 | 検証観点 | +| ------------------------- | -------------- | -------- | ------------------------ | +| **空文字列** | `""` | `0` | 基底条件 | +| **単一文字** | `"a"` | `1` | 最小ケース | +| **全て同じ文字** | `"bbbbb"` | `1` | ウィンドウが常にサイズ 1 | +| **全て異なる文字** | `"abcde"` | `5` | ウィンドウが最大まで拡大 | +| **繰り返しパターン** | `"abcabcbb"` | `3` | 左端の適切な前進 | +| **空白・記号混在** | `"p w!w@ke#w"` | `7` | 非英字の正しい扱い | +| **非 ASCII(日本語等)** | `"あいうあ"` | `3` | 辞書での管理 | +| **長大入力(5 万文字)** | ランダム生成 | 変動 | Time/Space 制約 | +| **ASCII と非 ASCII 混在** | `"a中b中c"` | `3` | 配列と辞書の併用 | + +### 検証スクリプト例 + +```python +def test_edge_cases(): + sol = Solution() + assert sol.lengthOfLongestSubstring("") == 0 + assert sol.lengthOfLongestSubstring("a") == 1 + assert sol.lengthOfLongestSubstring("bbbbb") == 1 + assert sol.lengthOfLongestSubstring("abcde") == 5 + assert sol.lengthOfLongestSubstring("abcabcbb") == 3 + assert sol.lengthOfLongestSubstring("pwwkew") == 3 + assert sol.lengthOfLongestSubstring("p w!w@ke#w") == 7 + assert sol.lengthOfLongestSubstring("あいうあ") == 3 + assert sol.lengthOfLongestSubstring("a中b中c") == 3 + print("All edge cases passed!") +``` + +--- + +

FAQ

+ +### Q1: なぜ `array('I')` を使うのか? + +**A**: `list[int]` に比べて以下のメリット: + +- **メモリ密度**: 4 バイト/要素で固定(list はポインタオーバーヘッドあり) +- **キャッシュ局所性**: 連続メモリ配置で高速アクセス +- **型保証**: 符号なし整数のみ格納(意図しない型混入を防止) + +### Q2: なぜ BMP 65536 配列を撤廃したのか? + +**A**: + +- **約 256KB のピークメモリ**が LeetCode のベンチマークで不利 +- 非 ASCII が少数の場合、辞書の方が **軽量**(出現種類数に比例) +- 速度への影響は軽微(辞書の平均 O(1)アクセスで十分高速) + +### Q3: `prev > left` の判定はなぜ正しいのか? + +**A**: + +- `prev` は **1-indexed**(`i+1` で記録) +- `left` は **0-indexed**(ウィンドウの左端) +- `prev > left` なら、前回出現位置は現在ウィンドウ内 → 左端を `prev` まで前進して重複を排除 + +### Q4: 全文字を `dict` のみにすると何が変わるか? + +**A**: + +- **メモリ**: さらに削減可能(ASCII も辞書管理 → 固定 512B も節約) +- **速度**: わずかに低下(配列アクセスより辞書が遅い) +- **トレードオフ**: メモリ優先なら有効、速度優先なら現在の実装が最適 + +### Q5: サロゲートペア(非 BMP, U+10000 以上)は? + +**A**: + +- Python の `ord()` は **コードポイント** を返す(サロゲートペア処理は不要) +- 非 BMP 文字も `code >= 128` で辞書に格納され、正しく扱われる + +### Q6: なぜ `i - left + 1` で長さが求まるのか? + +**A**: + +- ウィンドウは `[left, i]` の閉区間 +- 要素数 = `i - left + 1`(例: `[3, 5]` なら `5 - 3 + 1 = 3` 個) + +### Q7: `left` が単調増加する理由は? + +**A**: + +- `left = prev` で更新(`prev` は必ず `> left`) +- 一度前進した `left` は二度と後退しない → 単調性が保証される diff --git a/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.js b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.js new file mode 100644 index 00000000..c5f3c049 --- /dev/null +++ b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.js @@ -0,0 +1,281 @@ +// # 1. 問題の分析 + +// ## 競技プログラミング視点での分析 + +// * 典型問題「Longest Substring Without Repeating Characters」 +// * 最速は **スライディングウィンドウ+直近位置テーブル**。左右ポインタで 1 パス、各文字の最後の位置を参照して左端を更新。 +// * 入力長は最大 `5*10^4` なので **O(n)** で十分余裕。ハッシュ (`Map`) でも良いが、**連想配列よりも固定長の数値配列**の方が速く・省メモリになりやすい。 + +// ## 業務開発視点での分析 + +// * **関数は Pure**(入出力のみ・副作用なし)として実装。 +// * **入力検証**(型・長さ)を早期に実施し、`TypeError` / `RangeError` を明確に投げる。 +// * 命名は LeetCode 準拠の `lengthOfLongestSubstring` とし、保守性のため **JSDoc** を付与。 + +// ## JavaScript特有の考慮点 + +// * **V8 最適化**: + +// * 直近位置テーブルは `Uint32Array(65536)` を使用し、**「未出現 = 0」「出現位置は index+1」**で管理(`fill(-1)`等を避け初期化コストをゼロ化)。 +// * ループは `for (let i = 0; i < n; i++)` で単純化し、**hidden class** を安定化。 +// * **GC対策**: + +// * `Map`/オブジェクト生成を避け、**固定長 TypedArray** を再利用(関数内スコープで完結、都度の拡張なし)。 +// * **文字の扱い**: + +// * `charCodeAt` により **UTF-16 のコードユニット**単位で処理(一般的な LeetCode の想定と整合)。 +// * サロゲートペアを 1 文字として数えたい要件では追加実装が必要だが、本問題の制約(英数字・記号・スペース等)では十分。 + +// --- + +// # 2. アルゴリズムアプローチ比較 + +// | アプローチ | 時間計算量 | 空間計算量 | JS実装コスト | 可読性 | 備考 | +// | ----------------------------------- | -------- | ----------------------: | ------: | --- | ---------------------------- | +// | 方法A:スライディングウィンドウ+`Uint32Array` 直近位置 | **O(n)** | **O(1)**(固定 65536 × 4B) | 低 | 中 | 1 パス・分岐最小、初期化コスト小(未出現=0方式) | +// | 方法B:スライディング+`Map` | O(n) | O(min(n,Σ)) | 中 | 高 | 実装は直感的だが、GC/ハッシュ管理の分だけ相対的に不利 | +// | 方法C:二重ループ+集合チェック | O(n²) | O(1) | 低 | 高 | 小規模のみ妥当。最大入力では不適 | + +// > 注: 「JS実装コスト」はオブジェクト生成や型不安定化のリスクも加味。 + +// --- + +// # 3. 選択したアルゴリズムと理由 + +// * **選択**: 方法A(スライディングウィンドウ+`Uint32Array` 直近位置テーブル) +// * **理由**: + +// * **計算量**: 時間 **O(n)**・空間 **O(1)** を達成し、最大入力でも安定高速。 +// * **V8 友好**: 連続メモリの TypedArray でヒープ断片化や Map のオーバーヘッドを回避。 +// * **初期化最適化**: `index+1` を格納し「未出現=0」で管理するため `fill` 不要、起動コストが極小。 +// * **JavaScript特有の最適化ポイント**: + +// * `for` インデックス走査で JIT に優しいホットループ。 +// * 例外はホットパス外・冒頭で早期に投げる。 +// * 一時オブジェクト生成を抑制(クロージャ・コールバック不使用)。 + +// --- + +// # 4. コード実装(solution.js) + +// ```js +// 'use strict'; +// // Module: CommonJS(CI で `node solution.js` 実行可能) +// // 外部ライブラリ: 不使用(Node標準のみ) + +// /** +// * 長さが最大の「重複文字を含まない連続部分文字列」の長さを返す(Pure) +// * +// * 実装詳細: +// * - スライディングウィンドウ(left..i) +// * - 直近位置テーブル: Uint32Array(65536) に「最後に現れた位置+1」を格納(未出現=0) +// * - UTF-16 のコードユニット単位で処理(英数字・記号・スペースの制約に十分) +// * +// * @param {string} s - 入力文字列 +// * @returns {number} - 最長長さ +// * @throws {TypeError} - 入力が文字列でない場合 +// * @throws {RangeError} - 長さが制約 (0 <= len <= 5*10^4) を超える場合 +// * +// * @example +// * lengthOfLongestSubstring("abcabcbb") === 3 +// * lengthOfLongestSubstring("bbbbb") === 1 +// * lengthOfLongestSubstring("pwwkew") === 3 +// * +// * @complexity +// * 時間計算量: O(n) +// * 空間計算量: O(1)(固定 65536 要素のテーブル) +// */ +// function lengthOfLongestSubstring(s) { +// // --- 入力検証(軽量&早期) --- +// if (typeof s !== 'string') { +// throw new TypeError('Input must be a string'); +// } +// const n = s.length; +// if (n < 0 || n > 5 * 10 ** 4) { +// throw new RangeError('Input length out of allowed range (0..5*10^4)'); +// } +// if (n === 0) return 0; // 早期リターン + +// // --- 本処理 --- +// // 直近位置テーブル(UTF-16 コードユニット範囲: 0..65535) +// // 値は「最後に見た index + 1」。未出現は 0。 +// const lastPos = new Uint32Array(65536); + +// let left = 0; // 現在ウィンドウの左端 +// let best = 0; // 最長長さ + +// // 単純な for ループ(V8 好み、分岐少なめ) +// for (let i = 0; i < n; i++) { +// const code = s.charCodeAt(i); // UTF-16 コードユニット +// const seen = lastPos[code]; // 直近の出現位置+1(未出現=0) + +// if (seen > 0 && seen > left) { +// // 同じ文字がウィンドウ内にある → 左端を更新 +// left = seen; +// } +// // 現在位置(+1)を記録 +// lastPos[code] = i + 1; + +// // 長さ更新 +// const len = i - left + 1; +// if (len > best) best = len; +// } + +// return best; +// } + +// module.exports = { lengthOfLongestSubstring }; +// ``` + +// --- + +// # 5. 追加メモ(JS最適化チェックリスト) + +// * ループは `for` 基本。`forEach`/`map` は不要な関数割当を生むため回避。 +// * 一時オブジェクト・クロージャを作らない(GC 圧を低減)。 +// * 直近位置テーブルは **TypedArray** で固定長・数値単型を維持(hidden class 安定)。 +// * 例外はホットパス外(関数冒頭)で早期チェック。 +// * 文字は UTF-16 コードユニット基準。サロゲートペアを 1 文字換算にしたい場合は追加ロジックが必要(本問題の想定では不要)。 + +// # 1. 問題の分析 + +// ## 競技プログラミング視点での分析 + +// * 目標は **最速 & 低メモリ**。 +// * 基本はスライディングウィンドウ+「直近出現位置」。 +// * 多くの入力は **ASCII(≤127)** が大半 → ここを最適化すれば体感が大きく改善。 + +// ## 業務開発視点での分析 + +// * **Pure function** を維持(副作用なし)。 +// * **入力検証**を早期に行い、エラーはホットパス外で処理。 +// * 命名は LeetCode 準拠 `lengthOfLongestSubstring`。 + +// ## JavaScript特有の考慮点 + +// * **V8 最適化** + +// * まず **小さいテーブル(`Uint32Array(128)`)** で走らせ、**非ASCII(code ≥ 128)** を検知したら **一度だけ 65536 テーブルへ昇格**。 +// * 「未出現=0 / 出現は index+1」で**初期化不要**(`fill`を避ける)。 +// * ループは `for (let i = 0; i < n; i++)` のインデックス走査、関数割り当てなし。 +// * **GC対策** + +// * `Map`/オブジェクトを使わず、**TypedArray** で連続メモリ・単型数値。 +// * 昇格時のコピーは **128 要素だけ**(微小コスト)。 + +// --- + +// # 2. アルゴリズムアプローチ比較 + +// | アプローチ | 時間計算量 | 空間計算量 | JS実装コスト | 可読性 | 備考 | +// | -------------------------------------- | -------- | -------------------------------------: | ------: | --- | --------------------- | +// | 方法A(本回答): **ASCII最適化+必要時のみ 65536 に昇格** | **O(n)** | **O(1)**(通常 128×4B、非ASCII時のみ 65536×4B) | 低 | 中 | ほぼ ASCII の入力で速度・メモリ優位 | +// | 方法B: 常に `Uint32Array(65536)` | O(n) | O(1)(固定 65536×4B) | 低 | 中 | 実装簡単だが毎回大きい表を確保 | +// | 方法C: `Map` | O(n) | O(min(n,Σ)) | 中 | 高 | 直感的だが GC/ハッシュで相対的に不利 | + +// --- + +// # 3. 選択したアプローチと理由 + +// * **選択**: 方法A(ASCII 最適化+必要時のみ昇格) +// * **理由**: + +// * 実運用で多い ASCII 入力に対し **テーブル 128 要素で済む** → メモリ・キャッシュ効率良し。 +// * 非ASCII 混在でも **一度だけ昇格 & 128 要素コピー** でオーバーヘッド極小。 +// * `fill` 不要設計(index+1 管理)で **初期化コストゼロ**。 +// * **JS最適化ポイント**: + +// * 単純 `for` ループ、コールバック未使用(クロージャ生成なし)。 +// * TypedArray による単型・連続メモリで **JIT/CPU キャッシュに優しい**。 +// * 例外はホットパス外で早期判定。 + +// --- + +// # 4. コード実装(solution.js) + +// ```js +"use strict"; +// Module: CommonJS(node solution.js で実行可) +// 外部ライブラリ: 不使用(Node標準のみ) + +/** + * 長さが最大の「重複文字を含まない連続部分文字列」の長さを返す(Pure) + * + * 高速・省メモリの工夫: + * - まず Uint32Array(128)(ASCII)で処理し、非ASCII(code >= 128)に遭遇したら + * そのタイミングで一度だけ Uint32Array(65536) に「昇格」して継続。 + * - 値は「最後に見た index + 1」、未出現は 0(初期化 fill を回避)。 + * - UTF-16 のコードユニット単位(本問題の制約に十分)。 + * + * @param {string} s - 入力文字列 + * @returns {number} - 最長長さ + * @throws {TypeError} - 入力が文字列でない + * @throws {RangeError} - 長さが 0..5*10^4 を超える + * + * @complexity + * 時間計算量: O(n) + * 空間計算量: O(1) ※ 通常は 128×4B、非ASCIIを含む場合のみ 65536×4B + */ +function lengthOfLongestSubstring(s) { + // --- 入力検証(ホットパス外) --- + if (typeof s !== "string") throw new TypeError("Input must be a string"); + const n = s.length; + if (n < 0 || n > 5 * 10 ** 4) { + throw new RangeError("Input length out of allowed range (0..5*10^4)"); + } + if (n === 0) return 0; + + // --- 直近位置テーブル:まず ASCII 用(128 要素) --- + let lastPos = new Uint32Array(128); + let asciiOnly = true; // 昇格フラグ + let left = 0; + let best = 0; + + for (let i = 0; i < n; i++) { + const code = s.charCodeAt(i); + + // 非ASCIIに初めて遭遇 → 65536 テーブルへ一度だけ昇格 + if (asciiOnly && code >= 128) { + const big = new Uint32Array(65536); + // ここで 128 要素だけコピー(微小コスト) + big.set(lastPos); + lastPos = big; + asciiOnly = false; + } + + const prev = lastPos[code]; // 直近の出現位置+1(未出現=0) + if (prev > left) left = prev; // ウィンドウ左端を更新 + lastPos[code] = i + 1; // 現在位置+1 を記録 + + const len = i - left + 1; + if (len > best) best = len; + } + + return best; +} + +module.exports = { lengthOfLongestSubstring }; + +// ---- 以下はローカル実行の簡易入出力(CI想定: node solution.js "abcabcbb")---- +// 入力を与えない限りは何もしない(Pure実装とは分離) +if (require.main === module) { + const arg = process.argv[2]; + if (typeof arg === "string") { + console.log(lengthOfLongestSubstring(arg)); + } +} +// ``` + +// --- + +// # 5. 追加メモ(JS最適化チェックリスト) + +// * ループは `for`、コールバック不使用で **関数割当/クロージャ生成を回避**。 +// * **TypedArray** を用い、**index+1** 管理により `fill` を避ける(初期化ゼロコスト)。 +// * **ASCII 最適化**により小さい表(128)で走るケースが大半 → **メモリ/キャッシュ友好**。 +// * 非ASCIIが混在しても **一回だけ昇格 & 128要素コピー** のみ。 +// * 例外はホットパス外で早期に投げ、ホットループ内の分岐を最小化。 + +// > 補足:もしさらに「複数回呼ばれるワークロード」での効率を高めたい場合は、 +// > モジュールスコープに小テーブル/大テーブルを **再利用オブジェクト**として持ち、 +// > 「世代カウンタ(generation)」方式でクリアを省く最適化も可能です(本回答は関数単体の純粋性と可搬性を優先)。 diff --git a/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.py b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.py new file mode 100644 index 00000000..e7784a3a --- /dev/null +++ b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.py @@ -0,0 +1,286 @@ +# ### 1. 問題分析結果 + +# #### 競技プログラミング視点 + +# * **最速手法**: スライディングウィンドウ+「直近出現位置」記録で **O(n)**。 +# * **メモリ最小化**: 典型入力は ASCII が多い → まず **128 要素**だけ確保。非 ASCII が出た時だけ段階的に追加確保(BMP 用 65,536、さらに稀なサロゲート外は辞書)。無駄な初期化・コピーを避ける。 + +# #### 業務開発視点 + +# * **可読性・保守性**: `Solution.lengthOfLongestSubstring` に集約し、説明的な変数名と docstring。 +# * **型安全/エラー**: `s: str` を厳格に受け、型/長さの妥当性を早期チェック(LeetCode では想定外入力は来ないが、実務基準で堅牢化)。 + +# #### Python特有分析 + +# * **CPython 性能**: Python の `list` は PyObject 参照でメモリ過大になりがち。**`array('I')`** を使って C 連続メモリ(32bit 無符号整数)で **高速&省メモリ**にインデックス操作。 +# * **辞書は最小化**: 非 BMP(コードポイント ≥ 65536)に遭遇した場合のみ辞書を使用。 +# * **分岐最小化**: ホットループでは単純整数演算と添字アクセスのみ。 + +# --- + +# ### 2. アルゴリズム比較表 + +# | アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +# | ------------------------------------------------- | -------- | --------------------- | ----------- | --- | ------------ | ---------- | ----------------- | +# | 方法A: **段階確保**(ASCII 128 → BMP 65536 → 非BMPは dict) | **O(n)** | **O(1)**(入力集合に応じ段階確保) | 中 | 高 | `array('I')` | 適 | 本回答。最速・省メモリのバランス◎ | +# | 方法B: 常に BMP 65536 の表 | O(n) | O(1)(固定) | 低 | 中 | `array('I')` | 適 | 実装は楽だが毎回メモリ確保が大きい | +# | 方法C: 辞書のみ(`dict`) | O(n) | O(min(n,Σ)) | 低 | 高 | dict | 不適 | 実装は簡単だが相対的に遅・多メモリ | + +# --- + +# ### 3. 採用アルゴリズムと根拠 + +# * **選択**: 方法A(段階確保)。 +# * **理由**: + +# * 計算量は **O(n)** を維持しつつ、ASCII 入力での**超軽量メモリとキャッシュ効率**を享受。 +# * 非 ASCII も **必要時のみ**拡張、非 BMP は **辞書限定**で最小コスト。 +# * `array('I')` による **連続メモリ & C実装**で CPython に最適。 + +# **Python最適化ポイント** + +# * `array('I')` の 0 初期化を利用(`fill`不要)。 +# * 直近位置は **index+1** を格納(未出現=0 で分岐を簡素化)。 +# * 例外はホットパス外で早期判定、ループは整数添字と単純比較のみ。 + +# --- + +# ### 4. 実装コード(LeetCode 提出形式・Class 形式・型付け厳格/pylance対応) + +# ```python +# from __future__ import annotations + +# from array import array +# from typing import Dict, Final, Optional + + +# class Solution: +# """ +# Longest Substring Without Repeating Characters + +# スライディングウィンドウ + 直近出現位置の段階確保(ASCII → BMP → 非BMPは辞書)。 +# CPython では array('I') による連続メモリで低オーバーヘッド化。 +# """ + +# # 仕様上の上限(LeetCode 想定) +# _MAX_LEN: Final[int] = 5 * 10**4 + +# def lengthOfLongestSubstring(self, s: str) -> int: +# """ +# 連続部分文字列のうち、重複文字を含まない最長の長さを返す。 + +# Args: +# s: 入力文字列(str) + +# Returns: +# 最長長さ(int) + +# Raises: +# TypeError: 入力が文字列でない場合 +# ValueError: 入力長が仕様上限を超える場合 + +# Complexity: +# Time: O(n) / Space: O(1)(文字集合に応じ段階確保) +# """ +# # --- 入力検証(ホットパス外) --- +# if not isinstance(s, str): +# raise TypeError("Input must be a string") +# n: int = len(s) +# if n > self._MAX_LEN: +# raise ValueError("Input length exceeds allowed maximum") +# if n == 0: +# return 0 + +# # --- 段階確保テーブル --- +# # ASCII(0..127)は最初から確保(超軽量) +# last_ascii: array = array("I", [0]) * 128 + +# # BMP(0..65535)は必要になるまで確保しない +# last_bmp: Optional[array] = None # type: ignore[assignment] + +# # 非BMP(>= 65536)は非常に稀 → 初遭遇時のみ辞書を用意 +# last_large: Optional[Dict[int, int]] = None + +# left: int = 0 # ウィンドウ左端は「直近位置+1」を直接入れる運用 +# best: int = 0 + +# # --- メインループ(ホットパス) --- +# for i, ch in enumerate(s): +# code: int = ord(ch) + +# if code < 128: +# prev: int = last_ascii[code] +# if prev > left: +# left = prev +# last_ascii[code] = i + 1 + +# elif code < 65536: +# if last_bmp is None: +# # 必要時のみ BMP 用 65536 テーブルを確保(ゼロ初期化) +# last_bmp = array("I", [0]) * 65536 +# prev = last_bmp[code] # type: ignore[index] +# if prev > left: +# left = prev +# last_bmp[code] = i + 1 # type: ignore[index] + +# else: +# if last_large is None: +# last_large = {} +# prev = last_large.get(code, 0) +# if prev > left: +# left = prev +# last_large[code] = i + 1 + +# # 長さ更新 +# curr_len: int = i - left + 1 +# if curr_len > best: +# best = curr_len + +# return best +# ``` + +# --- + +# ### 5. 検証(方針のみ・コード不要) + +# * **境界**: `"" -> 0`, `"a" -> 1`, `"bbbbb" -> 1`, `"abcabcbb" -> 3`, `"pwwkew" -> 3`。 +# * **ASCII 専用パス**: 英数字+記号のみの長文でも O(n)/極小メモリで安定。 +# * **非 ASCII**: `"あいうえお"` や `"ééabc"` 混在で BMP パスが自動有効。 +# * **非 BMP**: 例えば `"𝟘𝟙𝟚abc"`(数学用太字数字など)のコードポイントでも辞書フォールバックで正しく動作。 + +# > メモ: LeetCode 環境は妥当な入力のみを与えるため、上記の型・長さチェックは実務基準の堅牢性として付与しています(競技では省略可)。 + +# 以下は原因の特定 → 具体的な修正案(ピークメモリ削減)→ 修正版コード(LeetCode提出形式 / Class形式)です。 + +# --- + +# ## 1) なぜメモリが大きく見えるのか(原因) + +# * **LeetCodeのメモリ計測はプロセス全体のRSS(実効使用量)**に近く、CPythonランタイムの常駐オーバーヘッド(インタープリタ本体、ヒープ管理、フレーム・コードオブジェクト等)が**十数MB**を占めます。 +# → これがベースラインなので、**18MB台は珍しくありません**。 + +# * その上で、前回の実装は「**非ASCII(U+0080..U+FFFF)に遭遇した場合に限り**」`array('I')` の **65536 要素表(約256KB)** を確保する設計でした。 +# → 256KBは小さいですが、**メモリ beat には効いてしまう**(他解が dict のみ等でピークを更に抑えている)。 + +# * 稀に**非BMP(サロゲートペア領域, U+10000以上)**が混じると `dict` も併用し、わずかに上乗せされます。 + +# 結論: **ベースライン(Python自体の常駐)+ BMP表 256KB** が主因。速度は速い一方で、**メモリ順位は相対的に不利**になりがちです。 + +# --- + +# ## 2) どう直すか(修正方針) + +# 目標:**速度は維持しつつピークメモリを削る**。 + +# ### 修正案A(おすすめ) + +# * **ASCIIは `array('I', 128)` 固定**で超軽量・高速を維持 +# * **非ASCIIは常に `dict` にフォールバック**(**BMP 65536表を作らない**) + +# * 非ASCIIが少数なら、辞書は必要なキー分しか増えず、**65536表の256KBを確実に回避** +# * 多くのケースで**メモリ beat が改善**します(速度への影響はごく軽微) + +# ### 修正案B + +# * **全文字 `dict` のみ**で管理(最少メモリ、最も単純) + +# * 速度はわずかに落ちるが、**メモリはさらに下がりやすい** +# * 速度が十分速い現在(Runtime 8ms, 97%ile)なら、メモリ順位を優先する場面で有効 + +# 以下では **修正案A** を実装します(現実的に速度とメモリの両立が良い)。 + +# --- + +# ## 3) 修正版コード(LeetCode提出形式 / Class形式 / Pylance対応) + +# ```python +from __future__ import annotations + +from array import array +from typing import Dict, Final + + +class Solution: + """ + Longest Substring Without Repeating Characters + + メモリ削減版: + - ASCII (0..127) は array('I', 128) の軽量表(未出現=0, 出現は index+1) + - 非ASCII (>=128) は dict に格納(BMP 65536表は使わない) + → 65536 要素配列(約256KB) の確保を完全に回避してピークメモリを下げる + """ + + _MAX_LEN: Final[int] = 5 * 10**4 + + def lengthOfLongestSubstring(self, s: str) -> int: + """ + Args: + s: 入力文字列 + + Returns: + 重複のない最長連続部分文字列の長さ + + Raises: + TypeError: s が str でない場合 + ValueError: 入力長が仕様上限を超える場合 + + Complexity: + Time: O(n) + Space: O(1) 相当(ASCIIは固定128、非ASCIIは出現数に比例) + """ + if not isinstance(s, str): + raise TypeError("Input must be a string") + n: int = len(s) + if n > self._MAX_LEN: + raise ValueError("Input length exceeds allowed maximum") + if n == 0: + return 0 + + # ASCII 用(0..127)だけ固定確保:極小&高速 + last_ascii: array = array("I", [0]) * 128 + # 非ASCII は dict にのみ格納(BMP 65536表は作らない) + last_other: Dict[int, int] = {} + + left: int = 0 + best: int = 0 + + for i, ch in enumerate(s): + code: int = ord(ch) + if code < 128: + prev: int = last_ascii[code] + if prev > left: + left = prev + last_ascii[code] = i + 1 + else: + prev = last_other.get(code, 0) + if prev > left: + left = prev + last_other[code] = i + 1 + + curr_len: int = i - left + 1 + if curr_len > best: + best = curr_len + + return best + + +# ``` + +# --- + +# ## 4) まとめ(効果とトレードオフ) + +# * **効果**: + +# * 以前の「非ASCIIで 65536 配列を確保」を**完全に撤廃** → **約256KBぶんのピーク削減** +# * 多くの入力でメモリ順位(Beats%)が改善しやすい +# * **速度**: + +# * 非ASCIIを `dict` で扱うぶん、ASCIIに比べると**わずかに遅い可能性**はありますが、 +# ASCII主体のケースは引き続き高速(`array` 参照のみ)。 +# * トータルでは **8msクラスの性能をほぼ維持**できます +# * **さらに削るなら**: + +# * **全文字 `dict` のみ(修正案B)**にすると、状況次第でピークをより下げられます(速度は微減) + +# 必要なら、**「速度最優先版(BMP 65536配列あり)」と「メモリ最小版(dictのみ)」の2実装を併記**して、ワークロードに合わせて切り替えるテンプレートもお渡しできます。 diff --git a/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.ts b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.ts new file mode 100644 index 00000000..7d6ac1eb --- /dev/null +++ b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/LongestSubstringWithoutRepeatingCharacters.ts @@ -0,0 +1,121 @@ +// ### 1. 問題の分析 + +// #### 競技プログラミング視点での分析 + +// * 目標は **最速 & 低メモリ**。 +// * 最適解は **スライディングウィンドウ + 直近出現位置テーブル** による 1 パス **O(n)**。 +// * 文字集合は実質 ASCII が多い前提が現実的 → **まず 128 エントリの表で走らせ、非 ASCII を検知したら一度だけ 65536 に昇格**(初期化コスト最小)。 + +// #### 業務開発視点での分析 + +// * **Pure function** で副作用なし、例外はホットパス外(関数冒頭)で早期に投げる。 +// * **型安全**: 引数は `string` に限定、戻り値は `number`。 +// * **保守性**: JSDoc と簡潔なロジック(単純な `for`)で読みやすさを担保。 + +// #### TypeScript特有の考慮点 + +// * **型推論**でローカル変数は `number` に収束(単型で JIT 友好)。 +// * **`readonly` を要求しない API 設計**(TS の型制約で過度なコピーを回避)。 +// * Node/TS では Unicode の完全なコードポイント処理も可能だが、本問題は **UTF-16 コードユニット**で十分(制約に合致)。 + +// --- + +// ### 2. アルゴリズムアプローチ比較 + +// | アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +// | -------------------------------------- | -------- | -------------------------------------- | ------- | ---- | --- | --------------------- | +// | 方法A: スライディング + **ASCII最適化→必要時昇格** | **O(n)** | **O(1)**(通常 128×4B、非ASCII時のみ 65536×4B) | 低 | 高 | 高 | 最速・省メモリ(本回答) | +// | 方法B: スライディング + `Uint32Array(65536)` 固定 | O(n) | O(1)(65536×4B 固定) | 低 | 高 | 中 | 実装容易だが毎回メモリ確保が大きい | +// | 方法C: スライディング + `Map` | O(n) | O(min(n,Σ)) | 中 | 高 | 高 | 直感的だが Map/GC のオーバーヘッド | + +// --- + +// ### 3. 実装方針 + +// * **選択したアプローチ**: 方法A(ASCII最適化 → 非ASCII検知時のみ一度だけ 65536 へ昇格) +// * **理由**: + +// * 計算量は O(n) を維持しつつ、**初期メモリが極小**でキャッシュ効率が高い。 +// * TS で TypedArray を使うことで **数値単型を保証**し、JIT/GC に優しい。 +// * 例外をホットパス外に押し出し、**ホットループの分岐を最小化**。 +// * **TypeScript特有の最適化ポイント**: + +// * 変数は `number` に統一、`const`/`let` で再割り当てパターンを安定化。 +// * JSDoc による契約明示で静的解析を強化。 +// * 余計なジェネリクスやユーティリティ型を使わず、**最小の表面積**で可読性と性能を両立。 + +// --- + +// ### 4. 実装コード(LeetCode フォーマット / ESM・外部ライブラリ不使用) + +// ```ts +// solution.ts (ESM) — Node.js v22.14.0 / TypeScript strict 推奨 +// 実行: tsc でビルド後、node で実行想定。LeetCode では関数だけ提出。 + +/** + * 長さが最大の「重複文字を含まない連続部分文字列」の長さを返す(Pure) + * + * 高速・省メモリの工夫: + * - まず Uint32Array(128)(ASCII)で走らせ、非ASCII(code >= 128)に遭遇したら + * 一度だけ Uint32Array(65536) へ「昇格」。128 要素だけ copy(微小コスト)。 + * - 値は「最後に見た index + 1」、未出現は 0(fill 回避で初期化コストゼロ)。 + * - UTF-16 のコードユニット単位(本問題の想定に十分)。 + * + * @param s - 入力文字列 + * @returns 最長長さ + * @throws {TypeError} 入力が文字列でない場合 + * @throws {RangeError} 長さが制約 (0..5*10^4) を超える場合 + * @complexity Time: O(n), Space: O(1) + */ +export function lengthOfLongestSubstring(s: string): number { + // --- 入力検証(ホットパス外) --- + if (typeof s !== "string") { + throw new TypeError("Input must be a string"); + } + const n: number = s.length; + if (n < 0 || n > 5 * 10 ** 4) { + throw new RangeError("Input length out of allowed range (0..5*10^4)"); + } + if (n === 0) return 0; + + // --- 直近位置テーブル:まず ASCII 用(128 要素) --- + let lastPos: Uint32Array = new Uint32Array(128); + let asciiOnly = true; // 初回の非ASCII検出で false にして昇格 + let left = 0; // ウィンドウ左端(index+1 を格納する運用) + let best = 0; + + // 単純 for ループ(JIT/CPU キャッシュに優しい) + for (let i = 0; i < n; i++) { + const code = s.charCodeAt(i); // UTF-16 コードユニット + + // 初めて非ASCIIに遭遇 → 一度だけ 65536 へ昇格 + if (asciiOnly && code >= 128) { + const big = new Uint32Array(65536); + big.set(lastPos); // 128 要素のみコピー + lastPos = big; + asciiOnly = false; + } + + const prev = lastPos[code]; // 直近出現位置+1(未出現=0) + if (prev > left) { + left = prev; // ウィンドウ左端を前進 + } + lastPos[code] = i + 1; // 現在位置+1 を記録 + + const len = i - left + 1; + if (len > best) best = len; + } + + return best; +} +// ``` + +// --- + +// ## TypeScript 固有の最適化観点(補足) + +// * **型推論**と**数値単型**で JIT 最適化を阻害しない(`number` のみ運用)。 +// * **TypedArray** による連続メモリで **GC 負荷を回避**、アクセスは添字のみで分岐最小。 +// * **例外はホットパス外**(冒頭)で早期に発火し、メインループのパフォーマンスを確保。 + +// > もし多数回呼び出すワークロードなら、モジュールスコープに再利用用の小/大テーブルを持ち、**世代カウンタ方式**でクリアを省く最適化も可能です(ただし LeetCode 提出の純粋性を優先し上記は単関数完結)。 diff --git a/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/README.md b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/README.md new file mode 100644 index 00000000..53b08763 --- /dev/null +++ b/Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/GPT/README.md @@ -0,0 +1,285 @@ +# Longest Substring Without Repeating Characters - 最長部分文字列長の計算 + +## Table of Contents + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython 最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +与えられた文字列から、**重複する文字を含まない最長の連続部分文字列の長さ**を求める問題です。 + +要件: + +- 入力: 文字列 `s`(長さは最大 50,000 程度) +- 出力: 最長部分文字列の長さ(整数) +- 制約: 高速な O(n) 時間で解く必要あり。 +- 安定性: 入力が空文字や特殊文字を含んでも正しく処理すること。 + +--- + +

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

+ +- スライディングウィンドウで左右ポインタを維持 +- ASCII 文字 (0..127) は固定長 `array('I', 128)` で直近出現位置を管理 +- 非 ASCII は `dict` に格納(65536 要素配列は使わずメモリ削減) +- 更新規則: 出現済みなら左ポインタを前回出現位置+1 に進める +- 各ステップで部分文字列長を更新して最大値を記録 +- **計算量**: Time O(n), Space O(1) 相当 + +--- + +

図解

+ +```mermaid +flowchart TD + Start[Start process] --> Init[Init ASCII table and dict] + Init --> Loop{More chars?} + Loop -- No --> Ret[Return max length] + Loop -- Yes --> Get[Read char and code] + Get --> Check{Code < 128} + Check -- Yes --> UseArray[Use ASCII array] + Check -- No --> UseDict[Use dict for non ASCII] + UseArray --> Update[Update left pointer if needed] + UseDict --> Update + Update --> Calc[Calc window length] + Calc --> Max[Update max length] + Max --> Loop +``` + +_スライディングウィンドウで逐次的に文字を処理し、ASCII は配列、非 ASCII は辞書で管理する流れを示す。_ + +```mermaid +graph LR + subgraph Precheck + A[Input string] --> B[Validate type] + B --> C[Init ASCII table] + B --> D[Init dict] + end + subgraph Core + C --> E[Sliding window] + D --> E + E --> F[Update pointers] + F --> G[Track max length] + end + G --> H[Output result] +``` + +_入力検証からデータ構造準備、スライディングウィンドウでの処理、最終結果出力までのデータフロー。_ + +--- + +

正しさのスケッチ

+ +- **不変条件**: ウィンドウ内は常に重複なし。 +- **基底条件**: 空文字なら結果は 0。 +- **更新規則**: 新しい文字が重複した場合、左ポインタを安全に進めることで重複を排除。 +- **終了性**: 各ステップで右ポインタは進み続け、有限長 n で必ず終了。 +- **網羅性**: 全ての部分文字列候補をウィンドウで走査するため、最大長を確実に記録できる。 + +--- + +

計算量

+ +- **時間計算量**: O(n) — 各文字を 1 回処理 +- **空間計算量**: O(1) — ASCII は固定 128、非 ASCII は出現数分の辞書 + +比較表: + +| 実装 | Time | Space | 備考 | +| --------------------- | ---- | ----------- | -------------------------- | +| 配列 + 辞書(本実装) | O(n) | O(1) | 高速かつ省メモリ | +| 配列 65536 固定 | O(n) | O(1) だが大 | 256KB の配列コスト | +| dict のみ | O(n) | O(k) | k=文字種数、速度は若干低下 | + +--- + +

Python 実装

+ +```python +from __future__ import annotations +from array import array +from typing import Dict, Final + +class Solution: + """ + Longest Substring Without Repeating Characters + + ASCIIは固定配列で管理し、非ASCIIはdictに保存。 + 65536配列を回避し、省メモリ化。 + """ + + _MAX_LEN: Final[int] = 5 * 10**4 + + def lengthOfLongestSubstring(self, s: str) -> int: + if not isinstance(s, str): + raise TypeError("Input must be a string") + n: int = len(s) + if n > self._MAX_LEN: + raise ValueError("Input length exceeds allowed maximum") + if n == 0: + return 0 + + # ASCII用の128長配列(未出現=0) + last_ascii: array = array("I", [0]) * 128 + # 非ASCIIは辞書に格納 + last_other: Dict[int, int] = {} + + left: int = 0 + best: int = 0 + + for i, ch in enumerate(s): + code: int = ord(ch) + if code < 128: + prev: int = last_ascii[code] + if prev > left: + left = prev + last_ascii[code] = i + 1 + else: + prev = last_other.get(code, 0) + if prev > left: + left = prev + last_other[code] = i + 1 + + curr_len: int = i - left + 1 + if curr_len > best: + best = curr_len + + return best +``` + +--- + +

CPython最適化ポイント

+ +- `for i, ch in enumerate(s)` を使いインデックスと文字を同時取得 → ループ高速化 +- `ord(ch)` を直接利用し、余計な変換を避ける +- `array('I')` は連続メモリで高速アクセス&GC 負担軽減 +- 例外チェック(型・長さ)はホットパス外にまとめることで本ループは軽量化 +- dict へのアクセスは `get` を利用して 1 ステップ化 + +--- + +

エッジケースと検証観点

+ +- 空文字列 → 出力 0 +- 全て同じ文字("aaaa")→ 出力 1 +- 全て異なる文字("abcd")→ 出力 len(s) +- 非 ASCII 文字("あいうえお")→ 辞書処理が正しく動作 +- 入力長が最大(50,000)→ O(n) 内で処理可能 + +--- + +

FAQ

+ +**Q1. メモリが他解より大きいのはなぜ?** +A. CPython 常駐コスト(十数 MB)が基準として計測されるため。配列 65536 要素を避けることで 256KB を削減済み。 + +**Q2. dict のみ実装と比べてどちらがよい?** +A. ASCII 主体なら配列が圧倒的に速い。本実装は ASCII 高速+非 ASCII 柔軟のバランス型。 + +**Q3. surrogate pair(U+10000 以上)は扱える?** +A. Python の `str` は UTF-32 に近いコードポイント管理をしており、`ord`で自然に処理できる。 + +## ループ処理について + +このループ部分はアルゴリズムの核心で、**各文字を読み取り → 直近の位置を更新 → ウィンドウ長を計算 → 最大値を更新**という流れをしています。 +これをより理解しやすいように、**図解付きで分解**して説明します。 + +--- + +## ステップごとの処理 + +```python +for i, ch in enumerate(s): + code: int = ord(ch) # 文字コードに変換 + if code < 128: # ASCII 文字? + prev: int = last_ascii[code] # 過去の出現位置 (0=未出現) + if prev > left: # 左ポインタを前に進める必要あり? + left = prev + last_ascii[code] = i + 1 # 現在位置を保存 + else: # 非ASCII文字 + prev = last_other.get(code, 0) + if prev > left: + left = prev + last_other[code] = i + 1 + + curr_len: int = i - left + 1 # ウィンドウ長 + if curr_len > best: # 最大値を更新 + best = curr_len +``` + +--- + +## 処理の流れ(フローチャート) + +```mermaid +flowchart TD + Start[Start loop over string] --> Read[Read char ch and index i] + Read --> Ord[Get code ord ch] + Ord --> Branch{code < 128 ?} + Branch -- Yes --> GetArr[prev from last_ascii] + Branch -- No --> GetDict[prev from last_other] + + GetArr --> Compare{prev > left ?} + GetDict --> Compare + + Compare -- Yes --> UpdateLeft[Update left = prev] + Compare -- No --> Skip[Keep left] + + UpdateLeft --> Save[Save i+1 as last seen] + Skip --> Save + + Save --> CalcLen[calc curr_len] + CalcLen --> CompareBest{curr_len > best ?} + CompareBest -- Yes --> UpdateBest[best = curr_len] + CompareBest -- No --> Continue[Skip update] + UpdateBest --> Continue + Continue --> LoopEnd{More chars ?} + LoopEnd -- Yes --> Read + LoopEnd -- No --> Return[Return best] +``` + +👉 この図から分かるように、 + +- **ASCII なら配列に保存** +- **非 ASCII なら辞書に保存** +- 直近出現位置が左境界より右にある場合は、ウィンドウを縮めて**重複を解消** +- その時点のウィンドウ長を計算し、最大値を更新 + という流れを繰り返しています。 + +--- + +## データ構造の状態イメージ + +入力例: `abcad` + +| i | ch | left | last_ascii/dict 更新 | curr_len | best | +| --- | --- | ---- | -------------------- | -------- | ---- | +| 0 | a | 0 | last_ascii['a']=1 | 1 | 1 | +| 1 | b | 0 | last_ascii['b']=2 | 2 | 2 | +| 2 | c | 0 | last_ascii['c']=3 | 3 | 3 | +| 3 | a | 1 | last_ascii['a']=4 | 3 | 3 | +| 4 | d | 1 | last_ascii['d']=5 | 4 | 4 | + +👉 `a` が 2 回目に出た時 (`i=3`)、`left` を `1` に進めて重複を回避。 +最後に `"bcad"` の長さ `4` が最大となる。 + +--- + +## まとめ + +- `left` は「現在のウィンドウの左端」を意味し、重複が見つかると更新されます。 +- `last_ascii`/`last_other` は「その文字が最後に出現したインデックス+1」を保持します。 +- `curr_len` を毎回計算し、最大値を `best` に保存することで最長長さを記録します。 + +--- From 76e82f6ee716fe897d6c2cb4f4cbfd9949a934b1 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sat, 4 Oct 2025 19:33:17 +0900 Subject: [PATCH 2/2] leetcode 178. Rank Scores --- .../178. Rank Scores/gpt/Rank Scores_mysql.md | 127 ++++++++ .../gpt/Rank Scores_pandas.md | 285 ++++++++++++++++++ .../gpt/Rank Scores_postgres.md | 127 ++++++++ 3 files changed, 539 insertions(+) create mode 100644 SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_mysql.md create mode 100644 SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_pandas.md create mode 100644 SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_postgres.md diff --git a/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_mysql.md b/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_mysql.md new file mode 100644 index 00000000..0a3a8e33 --- /dev/null +++ b/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_mysql.md @@ -0,0 +1,127 @@ +# 解説 + +## ✅ 解法 1(推奨 / MySQL 8.0+) + +```sql +SELECT + score, + DENSE_RANK() OVER (ORDER BY score DESC) AS `rank` +FROM Scores +ORDER BY score DESC; +``` + +### ポイント 1 + +- `DENSE_RANK()` は「同点は同順位」「次の順位は連番(穴なし)」を自動で満たします。 +- 問題要件の並び順は `ORDER BY score DESC` だけで OK(ID 順は不要)。 + +--- + +## ✅ 解法 2(互換 / MySQL 5.7 など) + +ウィンドウ関数が使えない環境では、**相関サブクエリ**で「自分より高い**異なる**スコアの個数 + 1」を順位にします。 + +```sql +SELECT + s1.score, + 1 + ( + SELECT COUNT(DISTINCT s2.score) + FROM Scores AS s2 + WHERE s2.score > s1.score + ) AS `rank` +FROM Scores AS s1 +ORDER BY s1.score DESC; +``` + +### ポイント 2 + +- `COUNT(DISTINCT s2.score)` により**同点は同じ順位**になる(穴なし= DENSE の性質)。 +- シンプルで確実ですが、データ量が多いと相関サブクエリで重くなることがあります。 + +--- + +## (参考)CTE で「重複除去 → 順位 → 突き合わせ」の段階を明示(MySQL 8.0+) + +概念的な段階(Distinct → Rank → Join)をクエリ上でも表したい場合: + +```sql +WITH ds AS ( + SELECT DISTINCT score FROM Scores +), +rr AS ( + SELECT + score, + DENSE_RANK() OVER (ORDER BY score DESC) AS `rank` + FROM ds +) +SELECT s.score, rr.`rank` +FROM Scores AS s +JOIN rr USING (score) +ORDER BY s.score DESC; +``` + +--- + +## 図解:処理の流れ(ウィンドウ関数版) + +> 余計な HTML タグや複雑な改行を排し、Mermaid が壊れにくい最小構成にしています。 + +```mermaid +flowchart TB + A[Scores 全行] --> B[降順ソート: score DESC] + B --> C[DENSE_RANK を各行に適用] + C --> D[結果を score DESC で出力] +``` + +--- + +## 図解:DENSE_RANK の考え方(概念) + +```mermaid +flowchart TB + A[スコア集合] --> B[等しいスコアをグループ化] + B --> C[高いスコアから順位を付与] + C --> D[同点は同じ順位] + D --> E[次の異なるスコアは直後の整数順位] +``` + +--- + +## サンプルに対する出力イメージ + +入力: + +```text +id | score +---+------- +1 | 3.50 +2 | 3.65 +3 | 4.00 +4 | 3.85 +5 | 4.00 +6 | 3.65 +``` + +出力(どの解法でも同じ): + +```text +score | rank +------+------ +4.00 | 1 +4.00 | 1 +3.85 | 2 +3.65 | 3 +3.65 | 3 +3.50 | 4 +``` + +--- + +## つまずきポイントと対策 + +- **表示の小数桁**:`score` が `DECIMAL(?,2)` であれば「4.00」のように 2 桁が保たれます。 + もし `FLOAT/DOUBLE` なら `ROUND(score, 2)` で揃えると安全です(問題文は DECIMAL 前提) +- **順位の穴**:`RANK()` は「穴あり」になるので、本問題は**必ず `DENSE_RANK()`** を使うか、相関サブクエリで「高い異なるスコア数 + 1」を計算してください。 +- **パフォーマンス**:巨大テーブルではウィンドウ関数が高速なことが多いです。5.7 の相関サブクエリ版はインデックス(`score`)である程度は緩和できます。 + +--- diff --git a/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_pandas.md b/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_pandas.md new file mode 100644 index 00000000..a427c9b4 --- /dev/null +++ b/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_pandas.md @@ -0,0 +1,285 @@ +# 解説 + +pandas で **同点は同順位・穴なし(= DENSE_RANK)** の順位付けを行う方法を示します。 +まずは最短・堅牢な実装(`rank(method="dense")`)、続いてウィンドウ関数不使用相当の段階的実装(重複除去 → 順位 → 突合)を載せます。最後に **Mermaid の壊れにくい最小構成**で図解します。 + +--- + +## ✅ 解法 1(最短・推奨:`rank(method="dense")`) + +```python +import pandas as pd + +def rank_scores(scores: pd.DataFrame) -> pd.DataFrame: + """ + Parameters + ---------- + scores : pd.DataFrame + 必須カラム: 'score'(数値。小数2桁相当) + + Returns + ------- + pd.DataFrame + カラム: ['score', 'rank'] + 並び: score の降順 + 性質: 同点は同順位、順位に穴なし(dense rank) + """ + df = scores.copy() + # 数値化と見た目の2桁丸め(必要なら) + df["score"] = pd.to_numeric(df["score"], errors="coerce") + # DENSE_RANK 相当 + df["rank"] = df["score"].rank(method="dense", ascending=False).astype(int) + + out = df.loc[:, ["score", "rank"]].sort_values( + by=["score", "rank"], ascending=[False, True] + ).reset_index(drop=True) + + # 表示を2桁に揃えたいだけなら(数値のまま保つならコメントアウト可) + out["score"] = out["score"].round(2) + return out +``` + +### 使い方(例) + +```python +scores = pd.DataFrame( + {"id": [1,2,3,4,5,6], "score": [3.50, 3.65, 4.00, 3.85, 4.00, 3.65]} +) +print(rank_scores(scores)) +``` + +出力(例): + +```text + score rank +0 4.00 1 +1 4.00 1 +2 3.85 2 +3 3.65 3 +4 3.65 3 +5 3.50 4 +``` + +--- + +## ✅ 解法 2(段階を明示:重複除去 → 順位 → 突合) + +```python +import pandas as pd + +def rank_scores_stepwise(scores: pd.DataFrame) -> pd.DataFrame: + """ + DENSE_RANK を明示的に段階計算: + 1) score を重複除去し降順に並べる + 2) その順に 1,2,3,... と順位を付ける + 3) 元の表に突合して結果を返す + """ + df = scores.copy() + df["score"] = pd.to_numeric(df["score"], errors="coerce") + + # 1) 重複除去して降順 + distinct = ( + df[["score"]].drop_duplicates().sort_values("score", ascending=False).reset_index(drop=True) + ) + # 2) 1 始まりの連番が DENSE_RANK + distinct["rank"] = distinct.index + 1 + + # 3) 元のデータに結合 + out = df.merge(distinct, on="score", how="left").loc[:, ["score", "rank"]] + out = out.sort_values(by=["score", "rank"], ascending=[False, True]).reset_index(drop=True) + out["score"] = out["score"].round(2) + return out +``` + +> この手法は SQL の「`SELECT DISTINCT` → `DENSE_RANK` → `JOIN`」の流れを pandas で再現したものです。 + +--- + +## 図解 1:処理の流れ(解法 1 のイメージ) + +```mermaid +flowchart TB + A[Scores 全行] --> B[score を降順に並べる] + B --> C[dense rank を適用] + C --> D[score 降順で結果を出力] +``` + +## 図解 2:DENSE_RANK の考え方 + +```mermaid +flowchart TB + S[スコア集合] --> G[同一スコアをグループ化] + G --> R[高いスコアから 1,2,3... と割当] + R --> T[同点は同じ順位] + T --> N[次の異なるスコアは直後の整数] +``` + +## 図解 3:段階実装(重複除去 → 順位 → 突合) + +```mermaid +flowchart TB + A[Scores] --> B[重複除去で score のみ] + B --> C[score を降順ソート] + C --> D[上から 1,2,3... を付与] + D --> E[Scores と score で結合] + E --> F[score 降順で整列] +``` + +--- + +## メモと実務 TIP + +- **同点の扱い**:`rank(method="dense")` で「同点=同順位・穴なし」が保証されます。 +- **小数表示**:見た目だけ 2 桁にしたいなら `round(2)` で十分。数値のまま集計継続したい場合は内部はそのままにして表示段階だけ整形すると安全です。 +- **欠損値**:`errors="coerce"` で非数値は `NaN` になります。要件次第で除外(`dropna`)や別処理を追加してください。 +- **安定性**:Mermaid は HTML タグや `
` を含めると壊れやすいので、上記のような最小構成がおすすめです。 + +ボトルネックの典型原因と、**高速・省メモリな置き換え実装**をまとめて提示します。 +結論から言うと、`Series.rank(method="dense")` は汎用で便利ですが、内部でソート+一時配列を複数回作るため、大量データではコストが嵩みます。 +**`factorize` や `np.unique(..., return_inverse=True)` を用いた実装**に差し替えると、時間・メモリともに大幅に改善します。 + +--- + +## まず疑うべき原因(診断チェックリスト) + +1. **dtype が object/str** + CSV 読み込み直後などで `score` が文字列のままだと、`rank`/`sort_values` が極端に遅くなります。 + → 読み込み時に `dtype={'score':'float32'}`(または後述の整数化)を指定。 + +2. **不要なコピーや二度ソート** + `df.copy()` の乱用、`sort_values` を何度も呼ぶ、`astype(int)` を早い段階で挟む、`merge` の往復などが余計なアロケーションを誘発。 + → **最終段だけ**整形・並び替えを行う。 + +3. **小数の丸めを先にやる** + `round(2)` や `to_char` 相当の整形を先にやると、再計算・コピーが増える。 + → 内部はそのまま、**表示直前のみ**丸める。 + +4. **相関サブクエリ相当の処理(groupby ループ/apply)** + Python レベルのループや `apply` は避ける。**完全ベクトル化**一択。 + +--- + +## 推奨修正 A:`factorize` で超高速 DENSE_RANK(最短) + +- 発想:**「値の出現パターンを符号化」**する `factorize` を、**降順**になるように工夫して使う +- ポイント:`factorize(..., sort=True)` は**ユニーク値をソートしてコードを付与**します。`-score` を渡すと、**「score の降順」=「-score の昇順」**になります。 + +```python +import pandas as pd +import numpy as np + +def rank_scores_fast_factorize(scores: pd.DataFrame) -> pd.DataFrame: + """ + DENSE_RANK を factorize で実現(高速・低メモリ) + 前提: scores['score'] は数値 dtype(float32/float64 推奨) + """ + df = scores[["score"]].copy() # 最小限のコピー + # 文字列なら読み込み時に dtype 指定。後からなら下行で変換(ただし一度だけ!) + # df["score"] = pd.to_numeric(df["score"], errors="coerce") + + # 降順の dense rank: -score を sort=True で factorize + codes, uniques = pd.factorize(-df["score"].to_numpy(), sort=True) + # codes は 0 始まりの連番。DENSE_RANK は 1 始まり: + df["rank"] = codes + 1 + + # 並びは要件通り score DESC のみ + out = df.sort_values("score", ascending=False, kind="mergesort").reset_index(drop=True) + # 表示だけ 2 桁にしたい場合 + out["score"] = out["score"].round(2) + return out +``` + +**なぜ速い?** + +- `factorize` は C 実装で、ユニーク化+コード化が一気に終わります。 +- `rank(method="dense")` より中間配列が少なく、巨大データで効果が出やすいです。 +- `mergesort` を指定しておけば安定ソートで同点行の相対順も一貫します(要件的にはどちらでも OK)。 + +--- + +## 推奨修正 B:**整数化(センチ化)+ `np.unique(..., return_inverse=True)`** + +- 発想:小数 2 桁が確定なら**整数(センチ)化**して比較・ユニーク化を高速化(浮動小数の誤差も回避) +- アルゴリズム: + + 1. `score` を `int_cents = np.rint(score*100).astype(np.int32)` に + 2. `uniques, inv = np.unique(int_cents, return_inverse=True)`(昇順ユニークと逆写像) + 3. **降順 dense rank** = `uniques.size - inv`(1 始まり) + +```python +import pandas as pd +import numpy as np + +def rank_scores_fast_unique(scores: pd.DataFrame) -> pd.DataFrame: + """ + DENSE_RANK を np.unique で実現(float→int 変換で高速・省メモリ) + """ + s = pd.to_numeric(scores["score"], errors="coerce").to_numpy() + # 小数2桁前提 → センチ化(四捨五入で誤差吸収) + cents = np.rint(s * 100).astype(np.int32) + + # 昇順ユニークと inverse インデックスを一発で作る + uniques, inv = np.unique(cents, return_inverse=True) # uniques: 昇順 + dense_desc_rank = uniques.size - inv # 1..k(降順) + + out = pd.DataFrame({ + "score": np.divide(cents, 100, dtype=np.float64), # 表示用に戻す + "rank": dense_desc_rank + }) + out = out.sort_values("score", ascending=False, kind="mergesort").reset_index(drop=True) + return out +``` + +## **メリット** + +- `float` 比較の不安を解消、`int32` でメモリ削減。 +- `np.unique(..., return_inverse=True)` は非常に速い。 +- 中間オブジェクト最小限。 + +--- + +## よくある落とし穴と修正 + +- **落とし穴 1:前処理で `round` してから rank** + → `round` は列コピーを生むので、**最後の表示整形だけ**に限定。 + +- **落とし穴 2:`merge` でユニーク表と突合** + → 小〜中規模なら OK だが、巨大データでは**`factorize` / `unique+inverse` で直接コード化**が圧倒的に軽い。 + +- **落とし穴 3:文字列 → 数値変換を毎回実行** + → 読み込み時 `pd.read_csv(..., dtype={'score':'float32'})` で固定。 + 2 桁固定なら `converters={'score': lambda x: int(Decimal(x)*100)}` で**最初から整数**にするのがベスト(ただし Decimal 依存)。 + +- **落とし穴 4:無駄な `.copy()` とチェーン** + → 大きな列に対しては**配列ビュー(`.to_numpy()`)で NumPy 直叩き**が軽い。 + +--- + +## どれくらい速くなる?(目安) + +- 100 万行・スコア重複ありのケースで、`rank(method="dense")` → `factorize` 置換で**2〜5 倍**速く、メモリ一時使用量も削減、というケースがよくあります(データ分布・環境依存)。 + +--- + +## 図解:高速化アプローチの流れ + +```mermaid +flowchart TB + A[Scores の score を数値化] --> B1[-score に変換して factorize ソート有効] + B1 --> C1[codes に 1 を加算して rank を作成] + A --> B2[score を 100 倍して四捨五入して int32 化] + B2 --> C2[unique で逆写像を取得] + C2 --> D2[rank は ユニーク個数 から 逆写像を減算] + C1 --> E[score を降順で並べて出力] + D2 --> E +``` + +--- + +## まとめ(即効リファクタ手順) + +1. **読み込み時 dtype 固定**(`float32` か **整数センチ**)。 +2. `rank(method="dense")` を **`factorize(-score, sort=True)+1`** か + **`np.unique(..., return_inverse=True)`** に置換。 +3. **並び替え・丸めは最後**に一度だけ。 +4. ヒープ圧迫が気になるなら、**`to_numpy()` で NumPy に寄せる**。 diff --git a/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_postgres.md b/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_postgres.md new file mode 100644 index 00000000..471e03b5 --- /dev/null +++ b/SQL/Leetcode/178. Rank Scores/gpt/Rank Scores_postgres.md @@ -0,0 +1,127 @@ +# 解説 + +## ✅ 解法 1(推奨 / PostgreSQL) + +```sql +-- PostgreSQL +SELECT + score, + DENSE_RANK() OVER (ORDER BY score DESC) AS rank +FROM scores +ORDER BY score DESC; +``` + +### メモ + +- PostgreSQL の小数は `NUMERIC(p,s)`(= MySQL DECIMAL 相当)。表示桁を固定したいだけなら `ROUND(score, 2)`、 + 文字列化までしたいなら `to_char(score, 'FM999990.00')` を使えます(ただし文字列になるので並び替えには使わないのが無難)。 + +--- + +## ✅ 解法 2(ウィンドウ関数なしの考え方) + +相関サブクエリで「自分よりも**大きい異なるスコア**の個数 + 1」を順位にします。**DENSE** の性質を守れます。 + +```sql +SELECT + s1.score, + 1 + ( + SELECT COUNT(DISTINCT s2.score) + FROM scores AS s2 + WHERE s2.score > s1.score + ) AS rank +FROM scores AS s1 +ORDER BY s1.score DESC; +``` + +--- + +## ✅ 解法 3(段階を明示:重複除去 → 順位付け → 結合) + +概念をクエリの構造にも反映したい場合の書き方です。 + +```sql +WITH distinct_scores AS ( + SELECT DISTINCT score + FROM scores +), +ranked AS ( + SELECT + score, + DENSE_RANK() OVER (ORDER BY score DESC) AS rank + FROM distinct_scores +) +SELECT s.score, r.rank +FROM scores AS s +JOIN ranked AS r USING (score) +ORDER BY s.score DESC; +``` + +--- + +## 図解 1:処理の流れ(ウィンドウ関数版) + +```mermaid +flowchart TB + A[Scores 全行] --> B[score を降順に並べる] + B --> C[DENSE_RANK を各行に適用] + C --> D[score 降順で結果を出力] +``` + +## 図解 2:DENSE_RANK の考え方 + +```mermaid +flowchart TB + A[スコア集合] --> B[同一スコアをグループ化] + B --> C[高いスコアから順位 1,2,3...] + C --> D[同点は同じ順位] + D --> E[次の異なるスコアは直後の整数] +``` + +## 図解 3:相関サブクエリ版のイメージ + +```mermaid +flowchart TB + X[行 s1 の score] --> Y[大きい score の集合 s2] + Y --> Z[異なる score の個数を数える] + Z --> W[個数 + 1 が s1 の順位] +``` + +--- + +## サンプル入出力(確認) + +入力(例) + +```text +id | score +---+------- +1 | 3.50 +2 | 3.65 +3 | 4.00 +4 | 3.85 +5 | 4.00 +6 | 3.65 +``` + +出力(いずれの解法でも同じ) + +```text +score | rank +------+------ +4.00 | 1 +4.00 | 1 +3.85 | 2 +3.65 | 3 +3.65 | 3 +3.50 | 4 +``` + +--- + +## つまずきやすい点と対策 + +- **順位の穴**:`RANK()` は同点のぶんだけ順位が飛ぶ(穴あり)。本問題は**必ず `DENSE_RANK()`** を使うか、相関サブクエリで「大きい**異なる**スコア数 + 1」を計算してください。 +- **表示桁**:結果を数値のまま返すならそのまま。 + 見た目を 2 桁固定にしたいなら `SELECT to_char(score, 'FM999990.00') AS score, ...` とし、並び替えは数値列 `score` を使うのが安全です。 +- **性能**:大量データではウィンドウ関数が一般に高速。相関サブクエリ版は `scores(score)` にインデックスがあると効果的です。