Skip to content

Commit 6992b3d

Browse files
authored
Merge pull request #160 from myoshi2891/dev/macbook_pro
Dev/macbook pro
2 parents e8bd10b + 76e82f6 commit 6992b3d

9 files changed

Lines changed: 3908 additions & 0 deletions

File tree

Algorithm/Sliding Window Method/leetcode/3. Longest Substring Without Repeating Characters/Claude/README.html

Lines changed: 2016 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
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 &lt; 128}
61+
CheckASCII -- Yes --> GetASCII[prev = last_ascii code]
62+
CheckASCII -- No --> GetDict[prev = last_other.get code, 0]
63+
GetASCII --> CheckPrev{prev &gt; 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 &gt; 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 &lt; 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

Comments
 (0)