Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions Python3/139. Word Break.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
## Step 1. Initial Solution

- 前からどんどん処理して残りの処理を委託するのが楽そうに見えるので再帰で試す
- 長い文字列に対してTLE → splitにかかる時間の問題?

```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
def isWordBreakable(s: str) -> bool:
if s in wordDict:
return True
for i in range(len(s)):
if s[:i+1] in wordDict and isWordBreakable(s[i+1:]):
return True
return False
return isWordBreakable(s)
```

- いくつか処理時間を短くする工夫を追加したがTLE
- 計算量の見積もりが難しかったが以下のように予想
- 関数内関数の実行回数は最悪の場合k^n
- かなりざっくりした見積もりなので流石にもっと少ないがオーダーはこれに近いので到底無理そう
- 20^300 ≈ 10^390

```python
# n: len(s), m: len(wordDict), k: max_word_length
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
max_word_length = len(max(wordDict, key=len)) # O(m)
wordDict = set(wordDict) # O(m)
def isWordBreakableAfter(start: int) -> bool:
if len(s) - start <= max_word_length and s[start:] in wordDict: # O(1) or O(k)
return True
word_length = min(max_word_length, len(s) - start)
for i in range(word_length, 0, -1): # O(k + (k + F(n-start))
if s[start:start + i] in wordDict and isWordBreakableAfter(start + i): # O(k) or O(k + F)
return True
return False
return isWordBreakableAfter(0) # O(m + k * k * ...) < O(k^n)
```

- 1時間くらい経ってしまったので答えを確認して以下の方針で実装
- wordDictに関してfor文を回す
- 各インデックスにそこまで行けるかを記録(DP)

```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
isWordBreakableTo = [True] + [False] * len(s)
for i in range(len(s)):
if not isWordBreakableTo[i]:
continue
for word in wordDict:
word_length = len(word)
if s[i:i + word_length] == word:
isWordBreakableTo[i + word_length] = True
return isWordBreakableTo[-1]
```

### Complexity Analysis

- 時間計算量:O(n * m * k)
- 文字列の長さ n * 辞書の長さ m * 単語の長さ k
- それぞれ最大 300 * 1000 * 20で 6 * 10^6 → 60 ms
- 空間計算量:O(n)
- DPの進捗保持

## Step 2. Alternatives

- 標準ライブラリのstartswith
- スライスを作らずに調べられるとのこと
- https://github.com/tokuhirat/LeetCode/pull/39/files#diff-97b706d7e4155f93440639c77521868dfea1408527ec8ac173776bf438145440R28
- tailmatchで一文字ずつ比較しているように読める
- https://github.com/python/cpython/blob/v3.6.1/Objects/unicodeobject.c#L13301-L13344
- BFSやDFSの実装も可能
- visitedを用いれば可能?
- https://github.com/Mike0121/LeetCode/pull/52/files#diff-1c85c7c43d808a526e18efee43b20161b4b539852849addbec75200ea7a322baR39
- 自分の元の実装案にvisitedを加えて実装したら行けた
- 関数内関数の実行回数は最大n回
- 再帰呼び出しを除けばO(k^2)
- 結果的にO(n * k^2)で実行可能
- もっと速くしたければwordDict内のwordの長さのsetを作っておいてfor文の中で長さに一致する単語が1個以上あるか判定することもできる

```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
max_word_length = len(max(wordDict, key=len))
wordDict = set(wordDict)
tried = set()
def isWordBreakableAfter(start: int) -> bool:
if (len(s) - start) <= max_word_length and s[start:] in wordDict:
return True
word_length = min(max_word_length, len(s) - start)
for i in range(word_length, 0, -1):
if start + i in tried:
continue
if s[start:start + i] in wordDict:
if isWordBreakableAfter(start + i):
return True
else:
tried.add(start + i)
return False
return isWordBreakableAfter(0)
```

- BFS・ループでも実装
- breakableToとtriedを保持しておくのもDPでTrue・Falseをつけていくのと情報的には同じ

```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words: set[str] = set()
word_lengths: set[int] = set()
for word in wordDict:
words.add(word)
word_lengths.add(len(word))
breakableTo: deque[int] = deque([0])
triedFrom: set[int] = set()
string_length = len(s)
while breakableTo:
start = breakableTo.popleft()
if start in triedFrom:
continue
triedFrom.add(start)
for i in word_lengths:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i より word_length の方が読みやすいと思いました。初め word_lengths の役割がわからなかったので、word_length_to_words = {文字数: list[単語]} のようにするとわかりやすくなるかもしれません。もしくはコメントで補足するのもありでしょうか。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確かにそれありですね。ありがとうございます!

if s[start:start + i] in words:
if start + i == string_length:
return True
breakableTo.append(start + i)
return False
```

- トップダウンの方が直感的という話、自分も共感した
- 300文字の中から単語を見つけろって言われたら流石に途中で作業を交代して欲しい
- https://github.com/Mike0121/LeetCode/pull/52/files#r1986286659
- Trie木でもできるという話
- https://github.com/tokuhirat/LeetCode/pull/39/files#diff-97b706d7e4155f93440639c77521868dfea1408527ec8ac173776bf438145440R92
- 今日はお腹いっぱいなので別日にやる

## Step 3. Final Solution

- DPでやるのがシンプルに書けそう
- 引継ぎ手順書を書きながら漏れなく確認を進めていく感じ
- s.startswithの前にスキップする分岐を入れることも考えたがコードが無駄に分かりにくくなるだけなのでやめた

```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
string_length = len(s)
breakableTo = [True] + [False] * string_length

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

変数名は snake case にするのが一般的だと思います。
https://peps.python.org/pep-0008/#function-and-variable-names
breakableTo のTo については意味を読み取ることができませんでした。
また、string_length は変数にするほど重要でない、len(s) より情報量が増えていない気がしました。

Copy link
Owner Author

@Satorien Satorien Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

breakableTo[i]とすれば英文的には分かりやすいのかなと思っていました
また、string_lengthについては二回呼び出すので保持しても良いのかなと思いましたが確かにない方が良いですかね

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ただの一意見ですが、自分もlen(s)をそのまま使ってしまって良いと思いました。
理由としては、len(s)はそもそも可読性が高い。string_lengthの変数名が少し長い。ためです。

for i in range(string_length):
if not breakableTo[i]:
continue
for word in wordDict:
if s.startswith(word, i):
breakableTo[i + len(word)] = True
return breakableTo[-1]
```