Skip to content

Commit d5c1a1c

Browse files
authored
Merge pull request #215 from myoshi2891/dev-from-macmini
Algorithm: 1D DP による空間最適化解法 97. Interleaving String
2 parents 2d886d0 + 7580300 commit d5c1a1c

6 files changed

Lines changed: 3329 additions & 1 deletion

File tree

.markdownlint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"MD002": true,
55
"MD003": { "style": "atx" },
66
"MD004": { "style": "consistent" },
7-
"MD007": { "indent": 2 },
7+
"MD007": { "indent": 4 },
88
"MD009": { "br_spaces": 2 },
99
"MD012": true,
1010
"MD013": {
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
# 97. Interleaving String - 1D DP による空間最適化解法
2+
3+
<h2 id="toc">目次(Table of Contents)</h2>
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+
**問題要約**
20+
21+
- 2つの文字列 `s1`, `s2` が与えられたとき、これらを interleave(交互に文字を取り出して結合)して `s3` を構成できるか判定する
22+
- interleaving の定義: `s1``s2` をそれぞれ部分文字列に分割し、順序を保ちながら交互に結合した結果が `s3` になること
23+
24+
**制約**
25+
26+
- `0 <= len(s1), len(s2) <= 100`
27+
- `0 <= len(s3) <= 200`
28+
- 全て小文字英字のみ
29+
30+
**要件**
31+
32+
- 正当性: `s1``s2` の文字順序を保ちつつ `s3` を構成できるか正確に判定
33+
- Follow-up: 空間計算量を `O(len(s2))` に抑える
34+
35+
---
36+
37+
<h2 id="tldr">アルゴリズム要点(TL;DR)</h2>
38+
39+
- **戦略**: 1次元 DP(列方向圧縮)による動的計画法
40+
- **データ構造**: `list[bool]` の 1次元配列 `dp`
41+
- `dp[j]`: `s1` の先頭 `i` 文字と `s2` の先頭 `j` 文字で `s3` の先頭 `i+j` 文字を構成できるか
42+
- **時間計算量**: `O(len(s1) * len(s2))`
43+
- **空間計算量**: `O(min(len(s1), len(s2)))` - Follow-up 要件を満たす
44+
- **最適化**: 短い方の文字列を DP の列方向に配置してメモリ効率を最大化
45+
46+
---
47+
48+
<h2 id="figures">図解</h2>
49+
50+
### フローチャート
51+
52+
```mermaid
53+
flowchart TD
54+
Start[Start solve] --> LenCheck{len s1 + len s2 == len s3}
55+
LenCheck -- No --> RetFalse[Return False]
56+
LenCheck -- Yes --> Swap{len s2 &gt; len s1}
57+
Swap -- Yes --> SwapStr[Swap s1 and s2]
58+
Swap -- No --> InitDP[Initialize dp array]
59+
SwapStr --> InitDP
60+
InitDP --> InitRow[Initialize i=0 row]
61+
InitRow --> LoopI[For i=1 to len s1]
62+
LoopI --> UpdateJ0[Update dp 0 for col j=0]
63+
UpdateJ0 --> LoopJ[For j=1 to len s2]
64+
LoopJ --> CheckS1{s1 i-1 == s3 k and dp j}
65+
CheckS1 -- Yes --> SetTrue1[from_s1 = True]
66+
CheckS1 -- No --> SetFalse1[from_s1 = False]
67+
SetTrue1 --> CheckS2
68+
SetFalse1 --> CheckS2{s2 j-1 == s3 k and dp j-1}
69+
CheckS2 -- Yes --> SetTrue2[from_s2 = True]
70+
CheckS2 -- No --> SetFalse2[from_s2 = False]
71+
SetTrue2 --> UpdateDP[dp j = from_s1 or from_s2]
72+
SetFalse2 --> UpdateDP
73+
UpdateDP --> NextJ{j &lt; len s2}
74+
NextJ -- Yes --> LoopJ
75+
NextJ -- No --> NextI{i &lt; len s1}
76+
NextI -- Yes --> LoopI
77+
NextI -- No --> RetResult[Return dp len s2]
78+
```
79+
80+
**説明**:
81+
82+
- 最初に長さチェックで不可能なケースを除外
83+
- `s2``s1` より長い場合は swap して、常に短い方を DP の列方向に配置
84+
- 各セル `dp[j]` は「`s1` の文字を使う」または「`s2` の文字を使う」のいずれかで遷移可能かを判定
85+
- 最終的に `dp[len(s2)]` が答え
86+
87+
### データフロー図
88+
89+
```mermaid
90+
graph LR
91+
subgraph Input_Phase
92+
A[Input s1 s2 s3] --> B[Length validation]
93+
B --> C[Swap if needed]
94+
end
95+
subgraph DP_Phase
96+
C --> D[Initialize dp array]
97+
D --> E[Fill i=0 row]
98+
E --> F[Iterate i from 1 to n1]
99+
F --> G[Update dp j for each j]
100+
G --> H[Check s1 or s2 match]
101+
end
102+
subgraph Output_Phase
103+
H --> I[Return dp n2]
104+
end
105+
```
106+
107+
**説明**:
108+
109+
- Input Phase: 入力検証と事前処理(swap含む)
110+
- DP Phase: DP テーブルの初期化と更新(1次元配列で in-place 更新)
111+
- Output Phase: 最終結果の返却
112+
113+
---
114+
115+
<h2 id="correctness">正しさのスケッチ</h2>
116+
117+
**不変条件**
118+
119+
- `dp[j]` は常に「現在の行 `i` における、`s1[:i]``s2[:j]``s3[:i+j]` を構成できるか」を保持
120+
- 各更新で前の行の情報(`dp[j]`: 上から、`dp[j-1]`: 左から)を参照して遷移
121+
122+
**網羅性**
123+
124+
- `i``0` から `len(s1)` まで走査し、各 `i``j``0` から `len(s2)` まで走査
125+
- 全ての組み合わせ `(i, j)` について DP 遷移を実行
126+
127+
**基底条件**
128+
129+
- `dp[0] = True`: 空文字列同士は interleave 可能
130+
- `i = 0` 行: `s1` を使わず `s2` のみで `s3` の先頭部分を構成できるか
131+
- `j = 0` 列: `s2` を使わず `s1` のみで `s3` の先頭部分を構成できるか
132+
133+
**終了性**
134+
135+
- ループは有限回(`O(len(s1) * len(s2))`)で必ず終了
136+
- 再帰なしの単純なループ構造
137+
138+
---
139+
140+
<h2 id="complexity">計算量</h2>
141+
142+
**時間計算量**: `O(len(s1) * len(s2))`
143+
144+
- 外側ループ: `len(s1)`
145+
- 内側ループ: `len(s2)`
146+
- 各ステップは定数時間の比較と代入のみ
147+
148+
**空間計算量**: `O(min(len(s1), len(s2)))`
149+
150+
- DP 配列のサイズ: `min(len(s1), len(s2)) + 1`
151+
- Follow-up の要件 `O(len(s2))` を満たす(短い方を列方向に配置するため実質 `O(min)`
152+
- 入力文字列以外の追加メモリはほぼ DP 配列のみ
153+
154+
**比較表: 2D vs 1D DP**
155+
156+
| 手法 | 時間計算量 | 空間計算量 | 実装難度 | CPython最適化 |
157+
| --------------- | ---------- | ------------- | -------- | ------------- |
158+
| 2D DP | O(n1\*n2) | O(n1\*n2) |||
159+
| 1D DP(本実装) | O(n1\*n2) | O(min(n1,n2)) |||
160+
161+
---
162+
163+
<h2 id="impl">Python 実装</h2>
164+
165+
```python
166+
from __future__ import annotations
167+
from typing import List
168+
169+
170+
class Solution:
171+
"""
172+
Interleaving String 判定クラス(LeetCode 用)
173+
174+
Time Complexity:
175+
O(len(s1) * len(s2))
176+
177+
Space Complexity:
178+
O(min(len(s1), len(s2))) # 1次元DP
179+
"""
180+
181+
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
182+
"""
183+
s3 が s1 と s2 の interleaving で構成できるかどうかを判定する。
184+
185+
Args:
186+
s1: 1つ目の文字列
187+
s2: 2つ目の文字列
188+
s3: 判定対象の文字列
189+
190+
Returns:
191+
s3 が s1 と s2 の interleaving なら True、それ以外は False
192+
"""
193+
n1: int = len(s1)
194+
n2: int = len(s2)
195+
n3: int = len(s3)
196+
197+
# 長さが合わなければ不可能
198+
if n1 + n2 != n3:
199+
return False
200+
201+
# dp の列方向(長さ)を常に「短い方の文字列」にする
202+
# → dp のサイズ縮小 + 内側ループ回数も減少
203+
if n2 > n1:
204+
# s1 を「長い方」、s2 を「短い方」に揃える
205+
s1, s2 = s2, s1
206+
n1, n2 = n2, n1
207+
208+
# dp[j]: s1 の先頭 i 文字と s2 の先頭 j 文字で s3 の先頭 i+j 文字を作れるか
209+
dp: List[bool] = [False] * (n2 + 1)
210+
211+
# i = 0 行(s1 を 0 文字使用)の初期化
212+
dp[0] = True
213+
for j in range(1, n2 + 1):
214+
dp[j] = dp[j - 1] and (s2[j - 1] == s3[j - 1])
215+
216+
# i >= 1 行の更新
217+
for i in range(1, n1 + 1):
218+
# j = 0 列(s2 を 0 文字使用)の更新
219+
dp[0] = dp[0] and (s1[i - 1] == s3[i - 1])
220+
221+
for j in range(1, n2 + 1):
222+
k: int = i + j - 1 # s3 のインデックス
223+
224+
# 上から来る: s1 の文字を使う
225+
from_s1: bool = dp[j] and (s1[i - 1] == s3[k])
226+
# 左から来る: s2 の文字を使う
227+
from_s2: bool = dp[j - 1] and (s2[j - 1] == s3[k])
228+
229+
dp[j] = from_s1 or from_s2
230+
231+
return dp[n2]
232+
```
233+
234+
**主要ステップのコメント**
235+
236+
1. **長さチェック**: `n1 + n2 != n3` なら即座に `False` を返却(必須の枝刈り)
237+
2. **Swap 最適化**: 短い方の文字列を列方向に配置してメモリとループ回数を削減
238+
3. **初期化**: `i = 0` 行を `s2` のみで `s3` を構成できるか判定
239+
4. **DP 更新**: 各 `(i, j)` で「`s1` から遷移」と「`s2` から遷移」の OR を取る
240+
5. **結果**: `dp[n2]` が最終的な答え
241+
242+
---
243+
244+
<h2 id="cpython">CPython最適化ポイント</h2>
245+
246+
1. **ローカル変数キャッシュ**
247+
- `n1`, `n2`, `n3` を事前計算してループ外に配置
248+
- `s1`, `s2`, `s3` も関数引数なので属性アクセスなし(すでに最適)
249+
250+
2. **短い文字列を列方向に配置**
251+
- `if n2 > n1: s1, s2 = s2, s1` により、常に小さい方が内側ループ
252+
- メモリアクセスパターンが改善し、キャッシュヒット率向上
253+
254+
3. **in-place 更新**
255+
- `dp` 配列を破壊的に更新(新リスト作成なし)
256+
- Python の `list` は C 配列ベースで高速なインデックスアクセス
257+
258+
4. **関数呼び出しの削減**
259+
- ループ内で `len()` などを呼ばない
260+
- 単純な算術演算とインデックスアクセスのみ
261+
262+
5. **型ヒント**
263+
- pylance による静的解析で型エラーを事前検出
264+
- 実行時のオーバーヘッドはないが、開発効率と保守性が向上
265+
266+
**追加の可能性(状況により)**
267+
268+
- `numba``@jit`: 数値計算主体でない文字列処理では効果薄
269+
- `lru_cache`: この問題は再帰でないため不要
270+
- `bisect`: ソート済みデータの探索がないため不要
271+
272+
---
273+
274+
<h2 id="edgecases">エッジケースと検証観点</h2>
275+
276+
1. **空文字列**
277+
- `s1 = ""`, `s2 = ""`, `s3 = ""``True`
278+
- `s1 = "a"`, `s2 = ""`, `s3 = "a"``True`
279+
- `s1 = ""`, `s2 = "b"`, `s3 = "b"``True`
280+
281+
2. **長さ不一致**
282+
- `s1 = "ab"`, `s2 = "cd"`, `s3 = "abc"``False`(即座に判定)
283+
284+
3. **同じ文字が複数存在**
285+
- `s1 = "aa"`, `s2 = "ab"`, `s3 = "aaba"` → 両方から `a` を取れるケースの正確な判定
286+
287+
4. **Example 1 (LeetCode)**
288+
- `s1 = "aabcc"`, `s2 = "dbbca"`, `s3 = "aadbbcbcac"``True`
289+
290+
5. **Example 2 (LeetCode)**
291+
- `s1 = "aabcc"`, `s2 = "dbbca"`, `s3 = "aadbbbaccc"``False`
292+
293+
6. **制約上限**
294+
- `len(s1) = 100`, `len(s2) = 100`, `len(s3) = 200` での性能確認
295+
- この規模でも `O(10,000)` の DP で十分高速
296+
297+
7. **片方が極端に短い**
298+
- `s1 = "a"`, `s2 = "b" * 100`, `s3 = ...` → swap 最適化の効果確認
299+
300+
---
301+
302+
<h2 id="faq">FAQ</h2>
303+
304+
**Q1: なぜ 2D DP ではなく 1D DP を使うのか?**
305+
306+
A: Follow-up の要件「`O(s2.length)` の追加メモリ」を満たすため。2D DP は `O(len(s1) * len(s2))` のメモリを消費するが、1D DP は各行を更新しながら進めるため、列方向の配列 1つ分のメモリで済む。
307+
308+
**Q2: swap 処理(短い方を列方向に配置)の効果は?**
309+
310+
A: 以下の2点で効率向上:
311+
312+
- メモリサイズが `min(len(s1), len(s2)) + 1` になる
313+
- 内側ループの回数が減る(短い方が内側ループになる)
314+
315+
実測では数 ms 〜 数 MB の改善だが、理論的に最適。
316+
317+
**Q3: 再帰 + メモ化ではダメなのか?**
318+
319+
A: 動作はするが、以下の理由でループベース DP が推奨:
320+
321+
- 再帰深度制限(Python のデフォルト 1000)に引っかかる可能性
322+
- 関数呼び出しオーバーヘッド
323+
- メモリレイアウトがループより非効率
324+
325+
**Q4: LeetCode で Runtime が揺れる理由は?**
326+
327+
A: オンラインジャッジの負荷やキャッシュ状態により、同じコードでも数 ms の揺れが発生。今回の実装は理論的に最適クラスなので、70% 〜 90% 程度の範囲で揺れるのは正常。
328+
329+
**Q5: 業務開発で使う際の注意点は?**
330+
331+
A:
332+
333+
- 型チェック(`isinstance(s1, str)` など)を追加
334+
- 長さ制約チェックを追加
335+
- docstring とログを充実させる
336+
- ドキュメント 1 の `isInterleave_production` を参考
337+
338+
**Q6: この問題の本質的な難しさは?**
339+
340+
A:
341+
342+
- 各位置で「`s1` の文字を使うか、`s2` の文字を使うか」の選択肢があり、全探索すると指数時間
343+
- DP により「同じ状態 `(i, j)` に複数の経路で到達する」ことを活用して多項式時間に削減
344+
- 1D DP への圧縮は「行方向の依存関係が1つ前の行のみ」という性質を利用
345+
346+
---
347+
348+
以上で、**LeetCode 97. Interleaving String** の 1D DP による最適化実装と、その背景にある理論・実装の詳細を網羅しました。この実装は Follow-up の要件を満たし、CPython での実行効率も高いトップレベルの解法です 🚀

0 commit comments

Comments
 (0)