Word Break

https://leetcode.com/problems/word-break/

Assume n = len(s), m = len(words), k is the average length of words.

Approach 1: BFS

s[start:end] in words indicates that there is an edge from start to end.

words = set(wordDict)
queue = deque([0])
seen = set()
 
while queue:
    start = queue.popleft()
    if start == len(s):  # reached the end
        return True
 
    for end in range(start + 1, len(s) + 1):
        if end in seen:  # only concerned about reaching end, not about how
            continue
 
        if s[start:end] in words:
            queue.append(end)
            seen.add(end)
 
return False

Time complexity: . Handling a node costs as we iterate over all the start before a specific end, and creates a string for each of the range. BFS can cost in total. Creating the set takes time.

Space complexity: .

Approach 2: Top-Down DP

lenth = [len(word) for word in words]
 
@cache
def dp(i):
    if i < 0:
        return True
    return any(
        s[i - l + 1:i + 1] == w and dp(i - l)
        for w, l in zip(words, length)
    )
 
return dp(len(s) - 1)

Time complexity: , there are states in table in total, to calculate each of them, we iterate over words, and perform substring operation on average length of .

Space complexity:

Approach 3: Bottom-Up DP

dp = [False] * len(s)
length = [len(w) for w in words]
 
for i in range(len(s)):
    for w, l in zip(words, length):
        if i < l - 1:  # word is too long
            continue
 
        if i == l - 1 or dp[i - l]:
            if s[i - l + 1:i + 1] == w:
                dp[i] = True
                break
 
return dp[-1]
  • i == l - 1: can reach i from start of string with this word
  • dp[i - l]: can reach the end of previous word

Time complexity: , states in total, time per state.

Space complexity:

Approach 4: Trie Optimization

class Trie(dict):
    def __init__(self, words):
        for w in words:
            self.insert(w)
        self.is_word = False
 
    def insert(self, word):
        node = self
        for c in word:
            node.setdefault(c, Trie())
            node = node[c]
        node.is_word = True
 
trie = Trie(words)
 
dp = [False] * len(s)
for i in range(len(s)):
    if i == 0 or dp[i - 1]:  # either at start or the previous word is reached
        node = trie
        for j in range(i, len(s)):
            c = s[j]
            if not node := node.get(c):
                break
            if node.is_word:
                dp[j] = True
 
return dp[-1]

Time complexity: . Building the trie iterates over all characters and takes . Once the trie is built, searching for a word takes . Nested loop iterates over indices, but each iteration stops after searches in trie on average.

Space complexity: , the trie can have at most nodes.

Approach 5: Another DP

dp[i] = True indicates a prefix of length i can be formed.

n = len(s)
dp = [False] * (n + 1)
dp[0] = True
 
for i in range(1, n + 1):
    for j in range(i):
        if dp[j] and s[j:i] in words:
            dp[i] = True
            break
 
return dp[-1]

Time complexity: , nested loop with substring operation takes time.