|
| 1 | +# Longest Substring Without Repeating Characters - スライディングウィンドウ+高速位置管理 |
| 2 | + |
| 3 | +## 目次 |
| 4 | + |
| 5 | +- [概要](#overview) |
| 6 | +- [アルゴリズム要点(TL;DR)](#tldr) |
| 7 | +- [図解](#figures) |
| 8 | +- [正しさのスケッチ](#correctness) |
| 9 | +- [計算量](#complexity) |
| 10 | +- [Python 実装](#impl) |
| 11 | +- [CPython 最適化ポイント](#cpython) |
| 12 | +- [エッジケースと検証観点](#edgecases) |
| 13 | +- [FAQ](#faq) |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +<h2 id="overview">概要</h2> |
| 18 | + |
| 19 | +**問題**: 文字列 `s` が与えられたとき、**重複文字を含まない最長の連続部分文字列**の長さを求める。 |
| 20 | + |
| 21 | +**要件**: |
| 22 | + |
| 23 | +- 入力長 `n`: `0 ≤ n ≤ 5×10⁴` |
| 24 | +- 文字種: 英字・数字・記号・空白(ASCII + 非 ASCII 混在可能) |
| 25 | +- **部分文字列**(substring)であり、部分列(subsequence)ではない → 連続性が必須 |
| 26 | + |
| 27 | +**制約**: |
| 28 | + |
| 29 | +- Time: **O(n)** を達成(各文字を高々 2 回走査) |
| 30 | +- Space: **O(min(n, Σ))** 相当(文字集合サイズ Σ に比例、ASCII なら固定 128) |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +<h2 id="tldr">アルゴリズム要点(TL;DR)</h2> |
| 35 | + |
| 36 | +- **戦略**: スライディングウィンドウ(右端 `i` を進めつつ、左端 `left` を必要に応じて前進) |
| 37 | +- **データ構造**: |
| 38 | + - **ASCII(0..127)**: `array('I', 128)` で固定・高速(未出現=0、出現は `index+1`) |
| 39 | + - **非 ASCII(≥128)**: `dict[int, int]` で管理(BMP 65536 配列を避けてメモリ削減) |
| 40 | +- **計算量**: |
| 41 | + - Time: **O(n)** — 各文字を高々 1 回ずつ走査+定数時間の位置更新 |
| 42 | + - Space: **O(1)** 相当(ASCII は固定 128、非 ASCII は出現数に比例) |
| 43 | +- **メモリ戦略**: 以前の BMP 65536 配列(約 256KB)を撤廃し、辞書に切り替えることで**ピークメモリを大幅削減** |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +<h2 id="figures">図解</h2> |
| 48 | + |
| 49 | +### フローチャート(処理の流れ) |
| 50 | + |
| 51 | +```mermaid |
| 52 | +flowchart TD |
| 53 | + Start[Start solve] --> Validate{Input valid} |
| 54 | + Validate -- No --> Raise[Raise error] |
| 55 | + Validate -- Yes --> Empty{n == 0} |
| 56 | + Empty -- Yes --> RetZero[Return 0] |
| 57 | + Empty -- No --> Init[Initialize left=0, best=0, last_ascii, last_other] |
| 58 | + Init --> Loop{For each i,ch in s} |
| 59 | + Loop -- No more --> Done[Return best] |
| 60 | + Loop -- Yes --> CheckASCII{code < 128} |
| 61 | + CheckASCII -- Yes --> GetASCII[prev = last_ascii code] |
| 62 | + CheckASCII -- No --> GetDict[prev = last_other.get code, 0] |
| 63 | + GetASCII --> CheckPrev{prev > left} |
| 64 | + GetDict --> CheckPrev |
| 65 | + CheckPrev -- Yes --> UpdateLeft[left = prev] |
| 66 | + CheckPrev -- No --> KeepLeft[Keep left] |
| 67 | + UpdateLeft --> StorePos[Store i+1 in last_ascii or last_other] |
| 68 | + KeepLeft --> StorePos |
| 69 | + StorePos --> CalcLen[curr_len = i - left + 1] |
| 70 | + CalcLen --> UpdateBest{curr_len > best} |
| 71 | + UpdateBest -- Yes --> SetBest[best = curr_len] |
| 72 | + UpdateBest -- No --> KeepBest[Keep best] |
| 73 | + SetBest --> Loop |
| 74 | + KeepBest --> Loop |
| 75 | + Done --> End[End] |
| 76 | +``` |
| 77 | + |
| 78 | +**説明**: |
| 79 | + |
| 80 | +1. 入力検証(型・長さ)を行い、空文字列なら即座に `0` を返す |
| 81 | +2. 各文字について、ASCII なら固定配列、非 ASCII なら辞書から前回出現位置を取得 |
| 82 | +3. 前回位置が現在ウィンドウ内(`prev > left`)なら左端を前進 |
| 83 | +4. 現在位置を記録し、ウィンドウ長を計算して最大値を更新 |
| 84 | +5. 全文字を処理後、最大長を返す |
| 85 | + |
| 86 | +### データフロー図 |
| 87 | + |
| 88 | +```mermaid |
| 89 | +graph LR |
| 90 | + subgraph Input_phase |
| 91 | + A[Input string s] --> B[Validate type and length] |
| 92 | + B --> C[Check n == 0] |
| 93 | + end |
| 94 | + subgraph Init_phase |
| 95 | + C --> D[Allocate last_ascii array 128] |
| 96 | + C --> E[Initialize last_other dict] |
| 97 | + end |
| 98 | + subgraph Core_loop |
| 99 | + D --> F[For each char i, ch] |
| 100 | + E --> F |
| 101 | + F --> G[Compute code = ord ch] |
| 102 | + G --> H{code < 128} |
| 103 | + H -- Yes --> I[Access last_ascii code] |
| 104 | + H -- No --> J[Access last_other code] |
| 105 | + I --> K[Update left if needed] |
| 106 | + J --> K |
| 107 | + K --> L[Store i+1 in last_ascii or last_other] |
| 108 | + L --> M[Calculate curr_len] |
| 109 | + M --> N[Update best] |
| 110 | + N --> F |
| 111 | + end |
| 112 | + F --> O[Return best] |
| 113 | +``` |
| 114 | + |
| 115 | +**説明**: |
| 116 | + |
| 117 | +- **Input_phase**: 入力の型チェック・長さ上限チェック・空文字列の早期リターン |
| 118 | +- **Init_phase**: ASCII 用固定配列(128 要素)と非 ASCII 用辞書を初期化 |
| 119 | +- **Core_loop**: 各文字について、コード値で分岐して ASCII 配列か辞書を参照し、ウィンドウを調整しながら最大長を更新 |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +<h2 id="correctness">正しさのスケッチ</h2> |
| 124 | + |
| 125 | +### 不変条件 |
| 126 | + |
| 127 | +**ウィンドウ `[left, i]` 内には重複文字が存在しない** |
| 128 | + |
| 129 | +- 各文字 `ch` について、その**前回出現位置 `prev`** を管理 |
| 130 | +- `prev > left` なら、`ch` が既にウィンドウ内に存在 → `left = prev` として左端を前進し、重複を排除 |
| 131 | +- `prev ≤ left` なら、`ch` はウィンドウ外またはまだ未出現 → ウィンドウを拡大可能 |
| 132 | + |
| 133 | +### 網羅性 |
| 134 | + |
| 135 | +- 右端 `i` を `0` から `n-1` まで全て走査 → すべての部分文字列の終端を試行 |
| 136 | +- 各 `i` で左端 `left` を適切に調整 → その終端での最長ウィンドウを常に維持 |
| 137 | +- よって、**すべての重複なし部分文字列の最大長を見逃さない** |
| 138 | + |
| 139 | +### 基底条件 |
| 140 | + |
| 141 | +- `n == 0` → `best = 0`(空文字列) |
| 142 | +- `n == 1` → ループ 1 回で `best = 1` |
| 143 | + |
| 144 | +### 終了性 |
| 145 | + |
| 146 | +- 外側ループは `i` を `0..n-1` まで増加 → 有限ステップで終了 |
| 147 | +- 各ステップは定数時間(配列または辞書への単一アクセス) |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +<h2 id="complexity">計算量</h2> |
| 152 | + |
| 153 | +### 時間計算量: **O(n)** |
| 154 | + |
| 155 | +- 各文字 `ch` について: |
| 156 | + - ASCII なら `last_ascii[code]` の **O(1)** アクセス |
| 157 | + - 非 ASCII なら `last_other.get(code, 0)` の **平均 O(1)** アクセス |
| 158 | +- `left` は単調増加(最大 `n` まで) → 全体で高々 `2n` ステップ |
| 159 | +- よって **線形時間** を達成 |
| 160 | + |
| 161 | +### 空間計算量: **O(min(n, Σ))** |
| 162 | + |
| 163 | +- **ASCII 部分**: `array('I', 128)` → **512 バイト固定**(4 バイト × 128) |
| 164 | +- **非 ASCII 部分**: `dict` は出現した非 ASCII 文字数に比例(最大 `n` だが実際は出現種類数) |
| 165 | +- トータル: **O(1) 相当**(多くの入力では ASCII 主体で辞書は小規模) |
| 166 | + |
| 167 | +### 比較表(修正前後) |
| 168 | + |
| 169 | +| 実装 | Time | Space(ASCII) | Space(非 ASCII 混在) | メモリ順位 | |
| 170 | +| ---------------------------- | ---- | -------------- | ---------------------- | ---------- | |
| 171 | +| **修正前**(BMP 65536 配列) | O(n) | 512B | 約 256KB(BMP 表) | 中程度 | |
| 172 | +| **修正後**(dict) | O(n) | 512B | 出現種類数に比例 | **改善** | |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +<h2 id="impl">Python 実装</h2> |
| 177 | + |
| 178 | +```python |
| 179 | +from __future__ import annotations |
| 180 | + |
| 181 | +from array import array |
| 182 | +from typing import Dict, Final |
| 183 | + |
| 184 | + |
| 185 | +class Solution: |
| 186 | + """ |
| 187 | + Longest Substring Without Repeating Characters |
| 188 | +
|
| 189 | + メモリ削減版: |
| 190 | + - ASCII (0..127) は array('I', 128) の軽量表(未出現=0, 出現は index+1) |
| 191 | + - 非ASCII (>=128) は dict に格納(BMP 65536表は使わない) |
| 192 | + → 65536 要素配列(約256KB) の確保を完全に回避してピークメモリを下げる |
| 193 | + """ |
| 194 | + |
| 195 | + _MAX_LEN: Final[int] = 5 * 10**4 |
| 196 | + |
| 197 | + def lengthOfLongestSubstring(self, s: str) -> int: |
| 198 | + """ |
| 199 | + Args: |
| 200 | + s: 入力文字列 |
| 201 | +
|
| 202 | + Returns: |
| 203 | + 重複のない最長連続部分文字列の長さ |
| 204 | +
|
| 205 | + Raises: |
| 206 | + TypeError: s が str でない場合 |
| 207 | + ValueError: 入力長が仕様上限を超える場合 |
| 208 | +
|
| 209 | + Complexity: |
| 210 | + Time: O(n) |
| 211 | + Space: O(1) 相当(ASCIIは固定128、非ASCIIは出現数に比例) |
| 212 | + """ |
| 213 | + # 入力検証 |
| 214 | + if not isinstance(s, str): |
| 215 | + raise TypeError("Input must be a string") |
| 216 | + n: int = len(s) |
| 217 | + if n > self._MAX_LEN: |
| 218 | + raise ValueError("Input length exceeds allowed maximum") |
| 219 | + |
| 220 | + # 基底条件: 空文字列 |
| 221 | + if n == 0: |
| 222 | + return 0 |
| 223 | + |
| 224 | + # ASCII 用(0..127)だけ固定確保:極小&高速 |
| 225 | + last_ascii: array = array("I", [0]) * 128 |
| 226 | + # 非ASCII は dict にのみ格納(BMP 65536表は作らない) |
| 227 | + last_other: Dict[int, int] = {} |
| 228 | + |
| 229 | + left: int = 0 # ウィンドウ左端 |
| 230 | + best: int = 0 # 最大長 |
| 231 | + |
| 232 | + for i, ch in enumerate(s): |
| 233 | + code: int = ord(ch) |
| 234 | + |
| 235 | + # 分岐: ASCIIか非ASCIIか |
| 236 | + if code < 128: |
| 237 | + prev: int = last_ascii[code] |
| 238 | + # 重複検出: 前回出現がウィンドウ内なら左端を前進 |
| 239 | + if prev > left: |
| 240 | + left = prev |
| 241 | + # 現在位置を記録(1-indexed: 0 は未出現) |
| 242 | + last_ascii[code] = i + 1 |
| 243 | + else: |
| 244 | + prev = last_other.get(code, 0) |
| 245 | + if prev > left: |
| 246 | + left = prev |
| 247 | + last_other[code] = i + 1 |
| 248 | + |
| 249 | + # 現在ウィンドウの長さを計算 |
| 250 | + curr_len: int = i - left + 1 |
| 251 | + if curr_len > best: |
| 252 | + best = curr_len |
| 253 | + |
| 254 | + return best |
| 255 | +``` |
| 256 | + |
| 257 | +### 補足 |
| 258 | + |
| 259 | +- **`array('I', [0]) * 128`**: 符号なし整数(4 バイト)× 128 = 512 バイトの固定配列 |
| 260 | +- **`last_other: Dict[int, int]`**: 非 ASCII 文字のコードポイント → 出現位置(1-indexed) |
| 261 | +- **1-indexed 記録**: `0` を「未出現」の番兵として利用し、`prev > left` 判定を単純化 |
| 262 | + |
| 263 | +--- |
| 264 | + |
| 265 | +<h2 id="cpython">CPython最適化ポイント</h2> |
| 266 | + |
| 267 | +### 1. **固定配列 `array('I')` で ASCII 高速化** |
| 268 | + |
| 269 | +- `list` や `dict` より**メモリ効率**と**キャッシュ効率**が高い |
| 270 | +- ASCII は 128 要素固定なので**オーバーヘッドが極小** |
| 271 | + |
| 272 | +### 2. **辞書は非 ASCII 専用(BMP 65536 配列の回避)** |
| 273 | + |
| 274 | +- 以前の実装では非 ASCII 出現時に 65536 要素配列(約 256KB)を確保 |
| 275 | +- 修正版では **`dict` にフォールバック** → 出現種類数に比例する軽量メモリ |
| 276 | +- **ピークメモリ削減** により、LeetCode のメモリ順位(Beats%)が改善しやすい |
| 277 | + |
| 278 | +### 3. **属性アクセス削減** |
| 279 | + |
| 280 | +- ループ内で `last_ascii[code]` を直接参照(メソッド呼び出しなし) |
| 281 | +- `last_other.get(code, 0)` は 1 回のみ(再取得しない) |
| 282 | + |
| 283 | +### 4. **早期リターン** |
| 284 | + |
| 285 | +- `n == 0` で即座に `0` を返し、不要なループを回避 |
| 286 | + |
| 287 | +### 5. **型注釈による静的解析** |
| 288 | + |
| 289 | +- pylance での型チェックを通過 → 実行時エラーを事前検出 |
| 290 | +- `Final` でクラス定数を明示 → 再代入防止 |
| 291 | + |
| 292 | +--- |
| 293 | + |
| 294 | +<h2 id="edgecases">エッジケースと検証観点</h2> |
| 295 | + |
| 296 | +| ケース | 入力例 | 期待出力 | 検証観点 | |
| 297 | +| ------------------------- | -------------- | -------- | ------------------------ | |
| 298 | +| **空文字列** | `""` | `0` | 基底条件 | |
| 299 | +| **単一文字** | `"a"` | `1` | 最小ケース | |
| 300 | +| **全て同じ文字** | `"bbbbb"` | `1` | ウィンドウが常にサイズ 1 | |
| 301 | +| **全て異なる文字** | `"abcde"` | `5` | ウィンドウが最大まで拡大 | |
| 302 | +| **繰り返しパターン** | `"abcabcbb"` | `3` | 左端の適切な前進 | |
| 303 | +| **空白・記号混在** | `"p w!w@ke#w"` | `7` | 非英字の正しい扱い | |
| 304 | +| **非 ASCII(日本語等)** | `"あいうあ"` | `3` | 辞書での管理 | |
| 305 | +| **長大入力(5 万文字)** | ランダム生成 | 変動 | Time/Space 制約 | |
| 306 | +| **ASCII と非 ASCII 混在** | `"a中b中c"` | `3` | 配列と辞書の併用 | |
| 307 | + |
| 308 | +### 検証スクリプト例 |
| 309 | + |
| 310 | +```python |
| 311 | +def test_edge_cases(): |
| 312 | + sol = Solution() |
| 313 | + assert sol.lengthOfLongestSubstring("") == 0 |
| 314 | + assert sol.lengthOfLongestSubstring("a") == 1 |
| 315 | + assert sol.lengthOfLongestSubstring("bbbbb") == 1 |
| 316 | + assert sol.lengthOfLongestSubstring("abcde") == 5 |
| 317 | + assert sol.lengthOfLongestSubstring("abcabcbb") == 3 |
| 318 | + assert sol.lengthOfLongestSubstring("pwwkew") == 3 |
| 319 | + assert sol.lengthOfLongestSubstring("p w!w@ke#w") == 7 |
| 320 | + assert sol.lengthOfLongestSubstring("あいうあ") == 3 |
| 321 | + assert sol.lengthOfLongestSubstring("a中b中c") == 3 |
| 322 | + print("All edge cases passed!") |
| 323 | +``` |
| 324 | + |
| 325 | +--- |
| 326 | + |
| 327 | +<h2 id="faq">FAQ</h2> |
| 328 | + |
| 329 | +### Q1: なぜ `array('I')` を使うのか? |
| 330 | + |
| 331 | +**A**: `list[int]` に比べて以下のメリット: |
| 332 | + |
| 333 | +- **メモリ密度**: 4 バイト/要素で固定(list はポインタオーバーヘッドあり) |
| 334 | +- **キャッシュ局所性**: 連続メモリ配置で高速アクセス |
| 335 | +- **型保証**: 符号なし整数のみ格納(意図しない型混入を防止) |
| 336 | + |
| 337 | +### Q2: なぜ BMP 65536 配列を撤廃したのか? |
| 338 | + |
| 339 | +**A**: |
| 340 | + |
| 341 | +- **約 256KB のピークメモリ**が LeetCode のベンチマークで不利 |
| 342 | +- 非 ASCII が少数の場合、辞書の方が **軽量**(出現種類数に比例) |
| 343 | +- 速度への影響は軽微(辞書の平均 O(1)アクセスで十分高速) |
| 344 | + |
| 345 | +### Q3: `prev > left` の判定はなぜ正しいのか? |
| 346 | + |
| 347 | +**A**: |
| 348 | + |
| 349 | +- `prev` は **1-indexed**(`i+1` で記録) |
| 350 | +- `left` は **0-indexed**(ウィンドウの左端) |
| 351 | +- `prev > left` なら、前回出現位置は現在ウィンドウ内 → 左端を `prev` まで前進して重複を排除 |
| 352 | + |
| 353 | +### Q4: 全文字を `dict` のみにすると何が変わるか? |
| 354 | + |
| 355 | +**A**: |
| 356 | + |
| 357 | +- **メモリ**: さらに削減可能(ASCII も辞書管理 → 固定 512B も節約) |
| 358 | +- **速度**: わずかに低下(配列アクセスより辞書が遅い) |
| 359 | +- **トレードオフ**: メモリ優先なら有効、速度優先なら現在の実装が最適 |
| 360 | + |
| 361 | +### Q5: サロゲートペア(非 BMP, U+10000 以上)は? |
| 362 | + |
| 363 | +**A**: |
| 364 | + |
| 365 | +- Python の `ord()` は **コードポイント** を返す(サロゲートペア処理は不要) |
| 366 | +- 非 BMP 文字も `code >= 128` で辞書に格納され、正しく扱われる |
| 367 | + |
| 368 | +### Q6: なぜ `i - left + 1` で長さが求まるのか? |
| 369 | + |
| 370 | +**A**: |
| 371 | + |
| 372 | +- ウィンドウは `[left, i]` の閉区間 |
| 373 | +- 要素数 = `i - left + 1`(例: `[3, 5]` なら `5 - 3 + 1 = 3` 個) |
| 374 | + |
| 375 | +### Q7: `left` が単調増加する理由は? |
| 376 | + |
| 377 | +**A**: |
| 378 | + |
| 379 | +- `left = prev` で更新(`prev` は必ず `> left`) |
| 380 | +- 一度前進した `left` は二度と後退しない → 単調性が保証される |
0 commit comments