cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 117 of 218
Ideas this builds on
1D dynamic programming117 / 218

Word Break II

Picture

For aaaa with words a and aa, “a a a a” and “aa aa” both work, along with other splits. The suffix feasibility idea still helps, but now every valid complete sentence must be returned.

Predict

What work can yes/no suffix DP safely remove without losing a sentence?

Walk through the small example

Branches whose remaining suffix cannot finish. It cannot discard different successful prefixes merely because they meet at the same suffix.

Mental model and complete Python solution

Use a trie to find word-end edges between boundaries. While filling suffix feasibility backward, retain only edges leading to a feasible suffix. Then backtrack through this pruned DAG, appending a word and undoing it after the recursive call. Every root-to-end boundary path produces one sentence. The DP never commits to only one successful edge. This shares discovery and rejects dead ends while preserving all requested outputs.

What the function promises: s is nonempty and dictionary words are nonempty and reusable. Return all distinct sentences in any order. The helper returns [] for empty s. The original problem bounds s to length 20; the enumeration recursion is intended for that small-input contract.

def build_trie(words):
    root = {}
    for word in set(words):
        if not word:
            raise ValueError("Dictionary words must be nonempty")
        node = root
        for letter in word:
            node = node.setdefault(letter, {})
        node[None] = True
    return root

def all_sentences(s, words):
    if not s:
        return []
    root = build_trie(words)
    n = len(s)
    can = [False] * (n + 1)
    can[n] = True
    following = [[] for _ in range(n)]
    for i in range(n - 1, -1, -1):
        node = root
        for j in range(i, n):
            if s[j] not in node:
                break
            node = node[s[j]]
            if None in node and can[j + 1]:
                following[i].append(j + 1)
                can[i] = True
    answer, path = [], []

    def emit(i):
        if i == n:
            answer.append(" ".join(path))
            return
        for end in following[i]:
            path.append(s[i:end])
            emit(end)
            path.pop()

    emit(0)
    return answer

assert sorted(all_sentences("cars", ["car", "ca", "rs"])) == ["ca rs"]
assert len(all_sentences("aaaa", ["a", "aa"])) == 5
assert all_sentences("cars", ["car"]) == []
assert all_sentences("a", ["a", "a"]) == ["a"]

Work and storage: Let B be the total characters across returned sentences. Trie/feasibility preparation takes expected O(W+nL) time and O(W+nL) space. Enumeration and string construction add O(B) output-sensitive work and storage, with O(n) recursion depth. B can be exponential in n.

When to recognize it: First mark suffixes that can finish. Then enumerate only word choices leading to those suffixes.

Check before applying it: A yes/no cache cannot replace the actual sentences. There may be exponentially many valid outputs.

Change one rule

We only need how many sentences exist. Must we emit them?

See what changes

No. Use a count per suffix, summing counts across word-end edges. That avoids enumerating each sentence, although large integer counts may themselves require more than constant-time arithmetic.

Connect

Word Break: Reuse the suffix feasibility answers to prune dead ends.

Word Search II: A trie filters choices while search still handles the larger state.

Problem reference: Word Break II (Hard). The walkthrough and Python solution above are original course material.