diff --git a/Python3/139. Word Break.md b/Python3/139. Word Break.md new file mode 100644 index 0000000..f2cdc8d --- /dev/null +++ b/Python3/139. Word Break.md @@ -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: + 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 + 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] +```