Dynamic Programming · Classic DP Patterns

Word Break (DP)

Building a 1D boolean table where dp[i] answers "can the first i characters of the string be fully segmented into dictionary words," checking every earlier valid breakpoint j where dp[j] is already true and the remaining piece s[j..i) is itself a dictionary word.

  • 5 min read
  • 8 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: fewest words, and why greedy fails
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Can a string with no spaces be split into dictionary words, the way "cookiejarcookie" splits into "cookie jar cookie"? Trying every split is exponential, since a string of n characters can be cut in 2^(n - 1) ways. But whether the rest of the string can be split depends only on where you are, not on how you got there. So ok[i] ("the first i characters can be split") is computed once per position, from earlier positions.

We'll go from simple to more technical:

  1. The pattern: ok[i] = some j with ok[j] and s[j..i) a word, starting from ok[0] = true.
  2. A second flavor: fewest words, and why greedy longest-match fails.
  3. Where segmentation runs in production (hashtags, search queries, languages without spaces).
  4. What breaks it: occurrence vs tiling, greedy, the empty start, slow lookups.

The approach

Java

boolean wordBreak(String s, List<String> wordDict) {
    Set<String> dict = new HashSet<>(wordDict);
    int n = s.length();
    boolean[] ok = new boolean[n + 1];              // ok[i] = the first i characters can be split
    ok[0] = true;                                   // the empty prefix
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < i; j++) {
            if (ok[j] && dict.contains(s.substring(j, i))) {   // split up to j, then one word j..i
                ok[i] = true;
                break;
            }
        }
    }
    return ok[n];
}

Python

def word_break(s: str, word_dict: list[str]) -> bool:
    words = set(word_dict)
    n = len(s)
    ok = [False] * (n + 1)                          # ok[i] = the first i characters can be split
    ok[0] = True                                    # the empty prefix
    for i in range(1, n + 1):
        for j in range(i):
            if ok[j] and s[j:i] in words:           # split up to j, then one word j..i
                ok[i] = True
                break
    return ok[n]

A prefix can be split exactly when some earlier prefix can be, followed by one dictionary word. That's the whole recurrence. It also explains a classic trap: "sunflowery" contains "sun", "flow", "flower", "sunflower" and "wer" as substrings, yet can't be split - the pieces must tile the string end to end, which is what chaining ok[j] into ok[i] enforces.

A second flavor: fewest words, and why greedy fails

Store a number instead of a boolean and the same table answers other questions. For the fewest words, best[i] = min over valid j of best[j] + 1, with "impossible" marked by a sentinel:

Java

Arrays.fill(best, Integer.MAX_VALUE);
best[0] = 0;
for (int i = 1; i <= n; i++)
    for (int j = Math.max(0, i - maxWordLen); j < i; j++)
        if (best[j] != Integer.MAX_VALUE && dict.contains(s.substring(j, i)))
            best[i] = Math.min(best[i], best[j] + 1);

Python

best = [float("inf")] * (n + 1)
best[0] = 0
for i in range(1, n + 1):
    for j in range(max(0, i - max_word_len), i):
        if best[j] != float("inf") and s[j:i] in words:
            best[i] = min(best[i], best[j] + 1)

A greedy "take the longest word at each position" is tempting and wrong. For "abcd" with {ab, abc, cd}, it takes "abc" and is stuck on "d", though "ab" + "cd" works. For "aaab" with {aa, aab, a, b}, it takes "aa", then "a", then "b" (3 words), while "a" + "aab" needs 2. Limiting j to the last maxWordLen positions also cuts the work: no word is longer than that.

Where you'll see it

Segmenting text without spaces - Chinese and Japanese tokenization, hashtags, domain names, URL slugs - is word-break DP, usually with scores (word frequencies) instead of booleans so the best segmentation wins. Validating that a byte stream is a sequence of known message formats, or that a code is a concatenation of valid tokens, is the same prefix reachability.

Which tool for which job?

You needUse
Can it be split?ok[i] over positions, j limited to the longest word length
Fewest wordsthe same table with min, infinity for impossible
How many waysthe same table with +, modulo if asked
The most likely splitmaximize the sum of log-probabilities of the words
Large dictionary, long stringwalk a trie forward from every reachable position
Every segmentationenumerate, pruned by "can this suffix be split?"

Common mistakes

MistakeWhat happensDo this instead
Checking that each word occurs somewhere"sunflowery" passes, though it can't be splitthe pieces must tile the string
Greedy longest matchfails on "abcd" with {ab, abc, cd}DP over every split point
ok[0] left falsenothing is ever reachablethe empty prefix is splittable
No word-length bound on jO(n³) character workj ≥ i - longest word
Boolean splits for real text"the rapist finder" is as good as "therapist finder"weight words by frequency

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. "sunflowery" with {sun, flow, flower, sunflower, wer}: every word appears in the string. Why can't it be split?

Show answer

The pieces must cover the string end to end. "sunflower" (or "sun" + "flower") leaves "y", and "sun" + "flow" leaves "ery", which no word starts. No chain of words reaches position 10, so ok[10] stays false even though every word occurs somewhere.

2. For "abcd" with {ab, abc, cd}, why does "take the longest word first" fail?

Show answer

It takes "abc" and is left with "d", which isn't a word. The DP instead marks positions 2 ("ab") and 3 ("abc") reachable, and from 2 reaches 4 with "cd". Greedy commits to one split; DP keeps every reachable split point.

3. How does the table change to find the fewest words?

Show answer

Store counts instead of booleans: best[0] = 0, "impossible" elsewhere, and best[i] = min over valid j of best[j] + 1. The answer is best[n], or -1 if it's still impossible.

Key takeaways

  • ok[i] = the first i characters can be split; ok[i] is true if some ok[j] is true and s[j..i) is a word.
  • ok[0] = true seeds everything; pieces must tile the string exactly.
  • Greedy longest-match fails; the DP checks every split point.
  • Swap the boolean for a count or minimum to count segmentations or find the fewest words.
  • Limit j to the longest word length (or use a trie) to cut the cost.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Splittable?ok[i], j bounded by the longest word
Fewest wordsmin version
Count ways+ version
Most likely splitmax of log-probabilities
Big dictionarytrie walk from reachable positions
All splitsenumerate through splittable suffixes

Snippets worth knowing by heart

Java

boolean[] ok = new boolean[n + 1];
ok[0] = true;
for (int i = 1; i <= n; i++)
    for (int j = Math.max(0, i - maxLen); j < i; j++)
        if (ok[j] && dict.contains(s.substring(j, i))) { ok[i] = true; break; }

Python

ok = [True] + [False] * n                # trie version
for j in range(n):
    if not ok[j]:
        continue
    node, i = trie, j
    while i < n and s[i] in node:
        node = node[s[i]]
        i += 1
        if "$" in node:
            ok[i] = True

The traps

  • Occurrence is not tiling: every word appearing somewhere doesn't mean the pieces cover the string. Check the pieces tile it.
  • Greedy longest match: "abcd" with {ab, abc, cd} fails after taking "abc". Try every split point.
  • ok[0] left false: nothing is ever reachable. The empty prefix is splittable.
  • No length bound: trying every j is O(n^3) character work. Look back only as far as the longest word.

Staff-level points, one line each

  • 1,133 characters: 243,036,235 characters hashed unbounded, 78,542 bounded, 5,975 trie steps.
  • Java: the unbounded version was at least 20x slower on "a" × 3000 + "b".
  • "a" × 30 with {a, aa} has 1,346,269 segmentations; "a" × 90 has about 4.66 × 10^18.
  • Weighted by frequency, the DP picked "therapist finder" out of 4 valid splits.

Say it in an interview

  • "ok[i] says the prefix can be split, and only the position matters, not the history."
  • "I bound j by the longest word, or walk a trie forward."
  • "Greedy longest match fails, so I compare every split point."
  • "For real text I'd maximize the words' log-probabilities."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Word Break (DP) free

Word Break

MediumProCoding problemJava · Python

Can the string s be cut into pieces that are all words from the dictionary (a word may be used more than once)? Return whether it can.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Word Break: Fewest Words

MediumProCoding problemJava · Python

Given a string s and a dictionary of words, split s into a sequence of dictionary words (each word may be reused) using as few words as possible. Return that number, or -1 if s can't be split at all.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Word Break (DP) free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime