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
191 changes: 191 additions & 0 deletions 139. Word Break/139. Word Break.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# 139. Word Break
## STEP1
- 何も見ずに解いてみる
- s を前から見ていき、wordDict で構成できるか確認する。
- word の文字数を考えるべきかどうかはパターンがありそう。スライスは文字列の長さを超えても問題ないので文字数を考慮しないことにした。
```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
is_break = [False] * (len(s) + 1)
is_break[0] = True
for i in range(len(s)):
if not is_break[i]:
continue
for word in wordDict:
if word == s[i : i + len(word)]:
is_break[i + len(word)] = True
return is_break[-1]
```
#### memo
時間計算量: $O(len(s) * len(wordDict) * max(len(word)))$
空間計算量: $O(len(s))$
計算時間を見積もる。CPU の動作周波数は数 GHz 程度で、1 秒間に数十億回機械語が実行される。C++ のような高速な言語だと、1 秒間に 1~10 億ステップ程度実行できる (コンパイルして機械語に変換したときのオーバーヘッドを考慮した数字)。Python は 50 倍程度遅く、1 秒間に 200~2000 万ステップ程度実行できる。s.length = 300, wordDict.length = 1000, wordDict[i].length = 20 の時を考えると、len(s) * len(wordDict) * max(len(word)) = 6 * 10^6 なので、計算時間は 0.3 ~ 3 s 程度。
LeetCode 上では 3 msだった。比較的単純な for 文であること、文字列に関してはネイティブなコードが走っていそう、という点で見積もりは大きくなった?

## STEP2
### プルリクやドキュメントを参照
- https://discord.com/channels/1084280443945353267/1225849404037009609/1245423153258299613
- startswith が使える。スライスしないで比較ができる。
- https://docs.python.org/3/library/stdtypes.html#str.startswith
```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
is_breakable = [False] * (len(s) + 1)
is_breakable[0] = True
for start_index in range(len(s)):
if not is_breakable[start_index]:
continue
for word in wordDict:
if s.startswith(word, start_index):
is_breakable[start_index + len(word)] = True
return is_breakable[-1]
```

- https://github.com/olsen-blue/Arai60/pull/39/files
- トップダウンでDPする書き方。STEP1 で書いたようなボトムアップの方が好み。
```python
import functools


class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
@functools.cache
def is_breakable(index: int) -> bool:
if index == 0:
return True
for word in wordDict:
start_index = index - len(word)
if start_index < 0:
continue
if s.startswith(word, start_index):
if is_breakable(start_index):
return True
return False

return is_breakable(len(s))
```
- https://github.com/Mike0121/LeetCode/pull/52/files
- DFS, BFS でも書ける。
- s の prefix が作れるかどうかを配列で管理して順番に走査するのがSTEP1の書き方で、作れる prefix を queue で管理するのが BFS。構成できるかのみを知りたい場合だとSTEP1の書き方がシンプルで好み。
```python
import collections


class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
start_indices = collections.deque([0])
checked_indices = {0}
while start_indices:
start_index = start_indices.popleft()
if start_index == len(s):
return True
for word in wordDict:
next_index = start_index + len(word)
if next_index in checked_indices:
continue
if s.startswith(word, start_index):
start_indices.append(next_index)
checked_indices.add(next_index)
return False
```
- https://github.com/philip82148/leetcode-swejp/pull/8/files#r1864310221
- Trie木というデータ構造を使うこともできる。
- DP で遷移を考える時に、O(len(wordDict) * max(len(wordDict[i]))) かかっていたところを改善できる。Trie木により wordDict の単語のうち prefix が同じものをまとめて管理できる。s の文字を Trie木で辿ることでDP の遷移を O(len(s)) で処理できる。

wordBreak 内で s と Trie木 をマッチングする書き方
```python
class TrieNode:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

単独のnodeの部分と、trieの木の部分とでクラスを分ける方が自分の好みです。

def __init__(self):
self.next = {}
self.is_end_word = False

def add_word(self, word):
node = self
for char in word:
if char not in node.next:
node.next[char] = TrieNode()
node = node.next[char]
node.is_end_word = True


class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
root = TrieNode()
for word in wordDict:
root.add_word(word)

is_breakable = [False] * (len(s) + 1)
is_breakable[0] = True
for start_index in range(len(s)):
if not is_breakable[start_index]:
continue
node = root
for i in range(start_index, len(s)):
if s[i] not in node.next:
break
node = node.next[s[i]]
if node.is_end_word:
is_breakable[i + 1] = True
return is_breakable[-1]
```

TrieNode.get_match_lengths を実装したパターン。こちらの方が機能が分離されていて良いと思った。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分もそう思いました

```python
class TrieNode:
def __init__(self):
self.next = {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

https://docs.python.org/ja/3/library/functions.html#next
nextが組み込み関数の名前と衝突してるので、ややこしいことになるかもしれません

self.is_word_end = False

def add_word(self, word):
node = self
for char in word:
if char not in node.next:
node.next[char] = TrieNode()
node = node.next[char]
node.is_word_end = True

def get_match_lengths(self, s):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ジェネレータにすることもできますね

result = []
node = self
for i, char in enumerate(s):
if char not in node.next:
break
node = node.next[char]
if node.is_word_end:
result.append(i + 1)
return result


class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
root = TrieNode()
for word in wordDict:
root.add_word(word)

is_breakable = [False] * (len(s) + 1)
is_breakable[0] = True
for start_index in range(len(s)):
if not is_breakable[start_index]:
continue
match_lengths = root.get_match_lengths(s[start_index:])
for length in match_lengths:
is_breakable[start_index + length] = True
return is_breakable[-1]
```
## STEP3
### 3回ミスなく書く
```python
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
is_breakable = [False] * (len(s) + 1)
is_breakable[0] = True
for start in range(len(s)):
if not is_breakable[start]:
continue
for word in wordDict:
if s.startswith(word, start):
is_breakable[start + len(word)] = True
return is_breakable[-1]
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

読みやすかったです。いいと思います。


3分,2分,2分で3回Accept