From b0eac6c46b5ef87ec23d1a886bb92bd653f42b8f Mon Sep 17 00:00:00 2001 From: tom4649 Date: Mon, 30 Mar 2026 06:49:35 +0900 Subject: [PATCH 1/4] 139. Word Break --- 139/memo.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 139/sol1.py | 14 ++++++++++ 139/sol1_failed.py | 17 +++++++++++ 139/sol2.py | 18 ++++++++++++ 139/sol3.py | 44 +++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 139/memo.md create mode 100644 139/sol1.py create mode 100644 139/sol1_failed.py create mode 100644 139/sol2.py create mode 100644 139/sol3.py diff --git a/139/memo.md b/139/memo.md new file mode 100644 index 0000000..e7e2fa2 --- /dev/null +++ b/139/memo.md @@ -0,0 +1,70 @@ +# 139. Word Break + +sol1.py: 時間制限に引っかかる。このコード以外にも複数ためしたが、wordDictを全探索して削るやり方だとうまくいかない +再帰関数を定義し、メモ化するようにしたらうまくいった + +https://discord.com/channels/1084280443945353267/1200089668901937312/1222092873508323368 + +> 0文字目から開始して、len(s)文字目に到達できれば受理します。 + +結局DFSで解けるのか。削除した文字列は数字で管理すれば良い。 + +> この問題、まず正規表現で書くことができるので O(n) で解けるはずとまず初めに考えました。 +> ((apple)|(pen))* +> 次に、その場合のよくある話として +> "a" * 51 +> は、 +> "a" * 2 と "a" * 4 で表せないので、単純なバックトラックでは失敗するというのが予想です。 + +「正規表現で書けるからO(n)」はあくまでwordDictが定数のときの話だろう。ただこの考えは持っておきたい。 + +> というわけで、先頭から DP が"模範解答"だろうな、とは思います。 + +> たとえば、priority queue を用意して、そこに数字 N が入っている場合は「先頭から N 文字目までの部分文字列は、wordDict の結合で表現できる」ということを意味する、とかします。 +> 初期値は [0] (0文字目までは表現できる。)ですね。 + + + +https://github.com/garunitule/coding_practice/pull/39 + +トライ木を使った実装 + +dataclass +https://docs.python.org/ja/3.10/library/dataclasses.html + +https://docs.python.org/3.10/library/dataclasses.html#dataclasses.field + +> default_factory: If provided, it must be a zero-argument callable that will be called when a default value is needed for this field. Among other purposes, this can be used to specify fields with mutable default values, as discussed below. It is an error to specify both default and default_factory. + +> This has the same issue as the original example using class C. That is, two instances of class D that do not specify a value for x when creating a class instance will share the same copy of x. Because dataclasses just use normal Python class creation they also share this behavior. There is no general way for Data Classes to detect this condition. Instead, the dataclass() decorator will raise a TypeError if it detects a default parameter of type list, dict, or set. This is a partial solution, but it does protect against many common errors. + +> Using default factory functions is a way to create new instances of mutable types as default values for fields: + +つまり、mutableな型を、クラス変数として置いたり、dataclass のフィールドのデフォルト値に直接書いたりするとインスタンス間で共有されてしまうことがある。dataclass も通常のPythonのクラス生成の仕組みに従うため基本的に同じだが、list/dict/set をデフォルトに直接指定した場合はTypeErrorを出して事故を減らす。default_factory を使えば、インスタンス生成のたびに新しいmutableを作って各インスタンスに持たせられる。 + + +https://github.com/mamo3gr/arai60/blob/139_word-break/139_word-break/step3_tuple_words.py + +> str.startswithがtuple[str]を受け取れる + +知らなかった。直感的にわかりやすい。 + + +計算量 +n = |s|, m = len(wordDict), l = max([len(word) for word in wordDoct])とする + +sol1.py +時間 O(nml): can_breakはメモ化しているので高々O(n)回呼び出される、それぞれの関数内でwordDict内全ての文字列比較をするので O(ml) +空間 O(n): 再帰スタックとメモ化 + +sol2.py +時間 O(nml): (visitedを使っているので)各単語の訪問回数は高々一回 O(n)、それぞれwordDictを全走査O(m)、文字列比較 O(l) +空間 O(n):visited, frontierの管理 + +sol3.py +時間 O((m+n)l): Trie木の構築 O(ml)、can_reachが全てTrueになった場合の探索 O(nl) +空間 O(ml+n): Trie木 O(ml)、can_reach O(n) + +sol2.pyは位置それぞれで文字列比較を行う分時間計算量が大きい。 + +sol2, sol3は自分で思いつけていないので後でやり直したい diff --git a/139/sol1.py b/139/sol1.py new file mode 100644 index 0000000..74713cb --- /dev/null +++ b/139/sol1.py @@ -0,0 +1,14 @@ +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + if not s: + return True + stripped_sub_strs = [] + for word in wordDict: + if s.startswith(word): + stripped_sub_strs.append(s[len(word) :]) + return any( + [ + self.wordBreak(stripped_sub_str, wordDict) + for stripped_sub_str in stripped_sub_strs + ] + ) diff --git a/139/sol1_failed.py b/139/sol1_failed.py new file mode 100644 index 0000000..16fdc36 --- /dev/null +++ b/139/sol1_failed.py @@ -0,0 +1,17 @@ +import functools + + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + len_target = len(s) + + @functools.cache + def can_break(i) -> bool: + if i == len_target: + return True + for word in wordDict: + if s.startswith(word, i) and can_break(i + len(word)): + return True + return False + + return can_break(0) diff --git a/139/sol2.py b/139/sol2.py new file mode 100644 index 0000000..4b615a3 --- /dev/null +++ b/139/sol2.py @@ -0,0 +1,18 @@ +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + frontier = [0] + visited = {0} + while frontier: + position = frontier.pop() + if position == len(s): + return True + for word in wordDict: + new_position = position + len(word) + if new_position in visited: + continue + if s[position:new_position] != word: + continue + frontier.append(new_position) + visited.add(new_position) + + return False diff --git a/139/sol3.py b/139/sol3.py new file mode 100644 index 0000000..4f70e02 --- /dev/null +++ b/139/sol3.py @@ -0,0 +1,44 @@ +import dataclasses +from typing import Dict, List + + +@dataclasses.dataclass +class TrieNode: + children: Dict[str, TrieNode] = dataclasses.field(default_factory=dict) + is_end: bool = False + + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + len_target = len(s) + if len_target == 0: + return True + + root = TrieNode() + max_len_of_wordDict = 0 + for word in wordDict: + max_len_of_wordDict = max(max_len_of_wordDict, len(w)) + node = root + for ch in word: + node = node.children.setdefault(ch, TrieNode()) + node.is_end = True + + can_reach = [False] * (len_target + 1) + can_reach[0] = True + + for i in range(len_target): + if not can_reach[i]: + continue + node = root + limit = min(len_target, i + max_len_of_wordDict) + for j in range(i, limit): + next_node = node.children.get(s[j]) + if next_node is None: + break + node = next_node + if node.is_end: + can_reach[j + 1] = True + if j + 1 == len_target: + return True + + return can_reach[len_target] From ab184356dd4e0b03ab6c77b0bbfd5faee4df5458 Mon Sep 17 00:00:00 2001 From: tom4649 Date: Mon, 30 Mar 2026 07:04:49 +0900 Subject: [PATCH 2/4] Improve readability of memo.md --- 139/memo.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/139/memo.md b/139/memo.md index e7e2fa2..572d1bd 100644 --- a/139/memo.md +++ b/139/memo.md @@ -50,20 +50,20 @@ https://github.com/mamo3gr/arai60/blob/139_word-break/139_word-break/step3_tuple 知らなかった。直感的にわかりやすい。 -計算量 -n = |s|, m = len(wordDict), l = max([len(word) for word in wordDoct])とする +## 計算量 +- n = |s|, m = len(wordDict), l = max([len(word) for word in wordDoct])とする -sol1.py -時間 O(nml): can_breakはメモ化しているので高々O(n)回呼び出される、それぞれの関数内でwordDict内全ての文字列比較をするので O(ml) -空間 O(n): 再帰スタックとメモ化 +### sol1.py +- 時間 O(nml): can_breakはメモ化しているので高々O(n)回呼び出される、それぞれの関数内でwordDict内全ての文字列比較をするので O(ml) +- 空間 O(n): 再帰スタックとメモ化 -sol2.py -時間 O(nml): (visitedを使っているので)各単語の訪問回数は高々一回 O(n)、それぞれwordDictを全走査O(m)、文字列比較 O(l) -空間 O(n):visited, frontierの管理 +### sol2.py +- 時間 O(nml): (visitedを使っているので)各単語の訪問回数は高々一回 O(n)、それぞれwordDictを全走査O(m)、文字列比較 O(l) +- 空間 O(n):visited, frontierの管理 -sol3.py -時間 O((m+n)l): Trie木の構築 O(ml)、can_reachが全てTrueになった場合の探索 O(nl) -空間 O(ml+n): Trie木 O(ml)、can_reach O(n) +### sol3.py +- 時間 O((m+n)l): Trie木の構築 O(ml)、can_reachが全てTrueになった場合の探索 O(nl) +- 空間 O(ml+n): Trie木 O(ml)、can_reach O(n) sol2.pyは位置それぞれで文字列比較を行う分時間計算量が大きい。 From 0d2d321f658afcca2526d73c6c4bb1d898fe21f0 Mon Sep 17 00:00:00 2001 From: tom4649 Date: Tue, 31 Mar 2026 06:37:32 +0900 Subject: [PATCH 3/4] Add suggested changes --- 139/sol1.py | 27 +++++++++++++++------------ 139/sol1_failed.py | 27 ++++++++++++--------------- 139/sol1_failed_revised.py | 14 ++++++++++++++ 139/sol1_revised.py | 18 ++++++++++++++++++ 4 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 139/sol1_failed_revised.py create mode 100644 139/sol1_revised.py diff --git a/139/sol1.py b/139/sol1.py index 74713cb..16fdc36 100644 --- a/139/sol1.py +++ b/139/sol1.py @@ -1,14 +1,17 @@ +import functools + + class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> bool: - if not s: - return True - stripped_sub_strs = [] - for word in wordDict: - if s.startswith(word): - stripped_sub_strs.append(s[len(word) :]) - return any( - [ - self.wordBreak(stripped_sub_str, wordDict) - for stripped_sub_str in stripped_sub_strs - ] - ) + len_target = len(s) + + @functools.cache + def can_break(i) -> bool: + if i == len_target: + return True + for word in wordDict: + if s.startswith(word, i) and can_break(i + len(word)): + return True + return False + + return can_break(0) diff --git a/139/sol1_failed.py b/139/sol1_failed.py index 16fdc36..74713cb 100644 --- a/139/sol1_failed.py +++ b/139/sol1_failed.py @@ -1,17 +1,14 @@ -import functools - - class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> bool: - len_target = len(s) - - @functools.cache - def can_break(i) -> bool: - if i == len_target: - return True - for word in wordDict: - if s.startswith(word, i) and can_break(i + len(word)): - return True - return False - - return can_break(0) + if not s: + return True + stripped_sub_strs = [] + for word in wordDict: + if s.startswith(word): + stripped_sub_strs.append(s[len(word) :]) + return any( + [ + self.wordBreak(stripped_sub_str, wordDict) + for stripped_sub_str in stripped_sub_strs + ] + ) diff --git a/139/sol1_failed_revised.py b/139/sol1_failed_revised.py new file mode 100644 index 0000000..eea9437 --- /dev/null +++ b/139/sol1_failed_revised.py @@ -0,0 +1,14 @@ +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + if not s: + return True + sub_strings = [] + for word in wordDict: + if s.startswith(word): + sub_strings.append(s[len(word) :]) + return any( + [ + self.wordBreak(stripped_sub_str, wordDict) + for stripped_sub_str in sub_strings + ] + ) diff --git a/139/sol1_revised.py b/139/sol1_revised.py new file mode 100644 index 0000000..72839e3 --- /dev/null +++ b/139/sol1_revised.py @@ -0,0 +1,18 @@ +import functools + + +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + len_target = len(s) + + @functools.cache + def can_break(i: int) -> bool: + """returns whether s[i:] can be broken.""" + if i == len_target: + return True + for word in wordDict: + if s.startswith(word, i) and can_break(i + len(word)): + return True + return False + + return can_break(0) From 94403bfb299cfa64f3a7c1fa9ddc168b9a64c5d2 Mon Sep 17 00:00:00 2001 From: tom4649 Date: Mon, 13 Apr 2026 06:59:47 +0900 Subject: [PATCH 4/4] Add suggested changes --- 139/sol1_revised.py | 8 ++++---- 139/sol2.py | 8 ++++---- 139/sol3.py | 3 +-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/139/sol1_revised.py b/139/sol1_revised.py index 72839e3..b9292f7 100644 --- a/139/sol1_revised.py +++ b/139/sol1_revised.py @@ -6,12 +6,12 @@ def wordBreak(self, s: str, wordDict: List[str]) -> bool: len_target = len(s) @functools.cache - def can_break(i: int) -> bool: - """returns whether s[i:] can be broken.""" - if i == len_target: + def can_break(start_pos: int) -> bool: + """returns whether s[start_pos:] can be broken.""" + if start_pos == len_target: return True for word in wordDict: - if s.startswith(word, i) and can_break(i + len(word)): + if s.startswith(word, start_pos) and can_break(start_pos + len(word)): return True return False diff --git a/139/sol2.py b/139/sol2.py index 4b615a3..d93947b 100644 --- a/139/sol2.py +++ b/139/sol2.py @@ -3,14 +3,14 @@ def wordBreak(self, s: str, wordDict: List[str]) -> bool: frontier = [0] visited = {0} while frontier: - position = frontier.pop() - if position == len(s): + start_position = frontier.pop() + if start_position == len(s): return True for word in wordDict: - new_position = position + len(word) + new_position = start_position + len(word) if new_position in visited: continue - if s[position:new_position] != word: + if s[start_position:new_position] != word: continue frontier.append(new_position) visited.add(new_position) diff --git a/139/sol3.py b/139/sol3.py index 4f70e02..7c571ae 100644 --- a/139/sol3.py +++ b/139/sol3.py @@ -15,9 +15,8 @@ def wordBreak(self, s: str, wordDict: List[str]) -> bool: return True root = TrieNode() - max_len_of_wordDict = 0 + max_len_of_wordDict = max(len(word) for word in wordDict) for word in wordDict: - max_len_of_wordDict = max(max_len_of_wordDict, len(w)) node = root for ch in word: node = node.children.setdefault(ch, TrieNode())