From 1928e9fdc101dd47f2112e381385bef8eb144cfd Mon Sep 17 00:00:00 2001 From: tokuhirat <54652919+tokuhirat@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:22:52 +0900 Subject: [PATCH] Create 139. Word Break.md --- 139. Word Break/139. Word Break.md | 191 +++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 139. Word Break/139. Word Break.md diff --git a/139. Word Break/139. Word Break.md b/139. Word Break/139. Word Break.md new file mode 100644 index 0000000..78e19ad --- /dev/null +++ b/139. Word Break/139. Word Break.md @@ -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: + 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 を実装したパターン。こちらの方が機能が分離されていて良いと思った。 +```python +class TrieNode: + def __init__(self): + self.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): + 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] +``` + +3分,2分,2分で3回Accept