|
| 1 | +""" |
| 2 | +Top-down dynamic programming with memoization. |
| 3 | +
|
| 4 | +Time Complexity: O(k * n * (n + m)), or O(k * n^2) when m <= n |
| 5 | + - n = len(s) |
| 6 | + - k = len(wordDict) |
| 7 | + - m = the maximum word length |
| 8 | +
|
| 9 | + There are at most n memoized states, and each state checks every word. |
| 10 | + In this implementation, s[index:] also creates a suffix whose length is |
| 11 | + O(n), while startswith may compare up to m characters. |
| 12 | +
|
| 13 | +Space Complexity: O(n) |
| 14 | + The memoization table and recursion stack each contain at most n entries. |
| 15 | +
|
| 16 | +dp(index) returns whether the suffix beginning at index can be completely |
| 17 | +segmented. Reaching len(s) means that all characters have been consumed. |
| 18 | +For every dictionary word that matches the current suffix, the function |
| 19 | +recursively checks the remaining suffix and stops at the first valid split. |
| 20 | +""" |
| 21 | +class Solution: |
| 22 | + def wordBreak(self, s: str, wordDict: List[str]) -> bool: |
| 23 | + @lru_cache() |
| 24 | + def dp(index: int) -> bool: |
| 25 | + if index == len(s): |
| 26 | + return True |
| 27 | + |
| 28 | + for word in wordDict: |
| 29 | + if len(s) - index < len(word): |
| 30 | + continue |
| 31 | + |
| 32 | + if s[index:].startswith(word) and dp(index + len(word)): |
| 33 | + return True |
| 34 | + |
| 35 | + return False |
| 36 | + |
| 37 | + return dp(0) |
| 38 | + |
| 39 | +""" |
| 40 | +Bottom-up dynamic programming over prefixes of s. |
| 41 | +
|
| 42 | +Time Complexity: O(k * n * (n + m)), or O(k * n^2) when m <= n |
| 43 | + - n = len(s) |
| 44 | + - k = len(wordDict) |
| 45 | + - m = the maximum word length |
| 46 | +
|
| 47 | + Each of the n + 1 prefix boundaries checks every word. The expression |
| 48 | + s[index - W:] creates a suffix of up to O(n) characters, and startswith |
| 49 | + may compare up to m characters. |
| 50 | +
|
| 51 | +Space Complexity: O(n) |
| 52 | + The dp array stores one value for every prefix boundary. |
| 53 | +
|
| 54 | +dp[index] indicates whether s[:index] can be segmented. dp[0] is true because |
| 55 | +the empty prefix needs no words. A word of length W can end at index when the |
| 56 | +preceding prefix, dp[index - W], is valid and the suffix beginning there starts |
| 57 | +with that word. The final entry represents the entire string. |
| 58 | +""" |
| 59 | +class Solution: |
| 60 | + def wordBreak(self, s: str, wordDict: List[str]) -> bool: |
| 61 | + S = len(s) |
| 62 | + |
| 63 | + dp = [0] * (S + 1) |
| 64 | + dp[0] = 1 |
| 65 | + |
| 66 | + for index in range(S + 1): |
| 67 | + for word in wordDict: |
| 68 | + W = len(word) |
| 69 | + if index < W or dp[index - W] == 0: |
| 70 | + continue |
| 71 | + if s[index - W :].startswith(word): |
| 72 | + dp[index] = dp[index - W] |
| 73 | + |
| 74 | + return bool(dp[-1]) |
| 75 | + |
| 76 | +""" |
| 77 | +Trie-assisted depth-first search over valid word boundaries. |
| 78 | +
|
| 79 | +Time Complexity: O(L + n^2) |
| 80 | + - n = len(s) |
| 81 | + - L = the total number of characters in wordDict |
| 82 | +
|
| 83 | + Building the trie takes O(L). Each reachable boundary is processed once, |
| 84 | + but a trie lookup may scan the remaining suffix, giving O(n^2) total work |
| 85 | + in the worst case. |
| 86 | +
|
| 87 | +Space Complexity: O(L + n) |
| 88 | + The trie uses O(L) space. The visited array, DFS stack, and a lookup's list |
| 89 | + of matching end positions use O(n) additional space. |
| 90 | +
|
| 91 | +startsWith(target, start) follows the trie from target[start:] and returns every |
| 92 | +exclusive end index at which a dictionary word finishes. The DFS treats those |
| 93 | +indices as the next segmentation boundaries. visited prevents the same boundary |
| 94 | +from being added to the stack more than once. |
| 95 | +""" |
| 96 | +class Trie: |
| 97 | + def __init__(self): |
| 98 | + self.children = dict() |
| 99 | + self.end = False |
| 100 | + |
| 101 | + def insert(self, target: int): |
| 102 | + node = self |
| 103 | + |
| 104 | + for ch in target: |
| 105 | + if ch not in node.children: |
| 106 | + node.children[ch] = Trie() |
| 107 | + node = node.children[ch] |
| 108 | + |
| 109 | + node.end = True |
| 110 | + |
| 111 | + def startsWith(self, target: str, start: int) -> List[int]: |
| 112 | + ans = [] |
| 113 | + node = self |
| 114 | + |
| 115 | + for i in range(start, len(target)): |
| 116 | + ch = target[i] |
| 117 | + |
| 118 | + if ch not in node.children: |
| 119 | + return ans |
| 120 | + |
| 121 | + node = node.children[ch] |
| 122 | + if node.end: |
| 123 | + ans.append(i + 1) |
| 124 | + |
| 125 | + return ans |
| 126 | + |
| 127 | + |
| 128 | +class Solution: |
| 129 | + def wordBreak(self, s: str, wordDict: List[str]) -> bool: |
| 130 | + S = len(s) |
| 131 | + t = Trie() |
| 132 | + visited = [False] * (S + 1) |
| 133 | + |
| 134 | + for word in wordDict: |
| 135 | + t.insert(word) |
| 136 | + |
| 137 | + stack = [0] |
| 138 | + |
| 139 | + while stack: |
| 140 | + start = stack.pop() |
| 141 | + |
| 142 | + starts_with = t.startsWith(s, start) |
| 143 | + |
| 144 | + for start_index in starts_with: |
| 145 | + if start_index == S: |
| 146 | + return True |
| 147 | + if visited[start_index]: |
| 148 | + continue |
| 149 | + visited[start_index] = True |
| 150 | + stack.append(start_index) |
| 151 | + |
| 152 | + return False |
0 commit comments