Abstract

Given a string of letters with no spaces or punctuation, how would you figure out how to separate it into words? (Determine if there is a way to separate it into a sequence of valid English words.)

  • Category: Dynamic Programming / String Processing
  • Input: A string of letters, and (implicitly) a dictionary or oracle that can check whether a given substring is a valid word
  • Output: Whether a valid word-split exists, and if so, one such split
  • Paradigm: Dynamic Programming
  • Typical use cases: tokenization/NLP preprocessing, spell-checking and autocomplete, any “can this sequence be decomposed into valid pieces” problem

Problem Specification

  • Instance: A string of letters, with no spaces or punctuation.
  • Solution Format: A boolean — does a valid word-split exist — plus, if true, the sequence of break points recovering the actual split.
  • Constraints: The substrings between consecutive break points must each individually be a valid dictionary word, and together must cover the entire string with no leftover characters.
  • Objective / Goal: Like Selection, this is a decision/existence problem rather than an optimization over many valid solutions — there’s no “better” split to maximize, just “does at least one valid split exist.”

Candidate Strategies / Approaches

Brute Force ✘

Try every possible way of placing word-breaks between the gaps in the string, then check whether every resulting piece is a valid word. There are ways to choose which gaps become breaks — exponential, and (just like Weighted Event Scheduling’s naive backtracking) this recomputes validity checks for the same substrings over and over across different candidate splits.

Dynamic Programming ✔

Same insight as Weighted Event Scheduling: define a small number of genuinely distinct sub-problems — “can the prefix ending at position be validly split?” — and solve them smallest-first, reusing each answer instead of re-deriving it. See The 8 Steps for the general recipe this follows.


Dynamic Programming Solution

1. Define the Array Values (Sub-Problems)

Let be true if can be separated into a sequence of English words, and false otherwise.

2. Base Case

(The empty prefix is vacuously a valid — empty — sequence of words.)

3. Express Recursively

4. Order the Problems

— each only ever depends on some with , so solving in increasing order of guarantees every dependency is already computed.

5. Output

6. Iterative Algorithm

Algorithm 20 String Reconstruction

procedure StringReconstruction(x[1n]x[1\dots n])

Initialize all S[.]S[.] to be False and all prev(.)prev(.) to be \emptyset

S(0)=trueS(0) = true

for kk from 11 to nn do

j=k1j = k-1

while not S(k)S(k) and j0j \geq 0 do

if S(j)S(j) is true and x[j+1k]x[j+1 \dots k] is a valid word then

S(k)S(k) = true

prev(k)=jprev(k) = j

else

j=j1j = j-1

if S(n)S(n) then

p=np = n

while p>0p > 0 do

print(pp)

p=prev(p)p = prev(p)

Fixed an Off-by-One

The source pseudocode’s inner loop condition was while not S(k) and j > 0, which means j is decremented down to (but never actually tests) j = 0. That’s a real bug: j = 0 is exactly the case “the entire prefix is itself a single valid word,” using the base case — a case that must be checked (e.g. for being the length of the very first word in the string). Changed the condition to j \geq 0 above so j=0 is actually tested before the loop exits.

Variables & Data Structures

NameTypePurpose
S[]Boolean array, size S[k] = whether the prefix has a valid word-split
prev[]Array, size prev[k] = the break point that produced S[k] = True, used to reconstruct the actual split
jIndexCandidate previous break point, checked from down to

Helper Functions / Operations Used

  • x[j+1...k] is a valid word — a dictionary lookup; if backed by a hash set, though extracting/hashing the substring itself costs .
  • Reconstruction via prev — once is known true, walk the prev pointers from back to , printing each break point; at most hops, so .

Proof of Correctness / Optimality

Claim: is set to True if and only if can be validly split into a sequence of words.

  • Base case: — the empty prefix trivially has a valid (empty) split.
  • Inductive Hypothesis: for all , is set correctly.
  • Inductive Step: the algorithm sets exactly when it finds some with (correct, by the Inductive Hypothesis) and a valid word. This matches the recursive definition in Step 3 directly: should be true iff some such exists. Since the (corrected) loop checks every from down to inclusive, it examines every possible split point — so it sets if and only if a valid actually exists.

Why the off-by-one mattered for correctness: without checking , the algorithm would incorrectly conclude for any where the only valid split has the entire prefix as a single word — a real, not just cosmetic, correctness gap.


Time & Space Complexity Analysis

General Case

ComplexityNotes
Time to Outer loop runs times; inner while loop checks up to candidate values of ; each check’s word-validity lookup costs with a precomputed hash set (giving total) or if the substring must be extracted/hashed fresh each time (giving worst case)
SpaceS[] and prev[] are both size

Best / Worst / Average Case

  • Best case: — if every prefix is only ever split at the immediately preceding position ( always works first), each while loop exits after one check.
  • Worst case: as above ( depending on the word-lookup cost) — occurs when many candidate values must be tried before (or without) finding a valid split.
  • Average case: depends heavily on the actual dictionary and input string; not meaningfully different from the worst case in general without further assumptions.

Drawbacks / Constraints

  • Depends on an unspecified dictionary/oracle. The algorithm assumes “is a valid word” can be checked, but doesn’t specify how the dictionary itself is represented or looked up — this is where the real-world implementation cost hides (see complexity table above).
  • Finds one split, not all of them. Since prev[k] stores only the first (highest) found, the algorithm can’t enumerate every possible decomposition when a string is ambiguous (e.g. a string splittable multiple different ways) — it just proves existence and recovers one witness.
  • No notion of “best” split. If disambiguating between multiple valid splits matters (e.g. preferring fewer, longer words, or the most common words), this needs to become an optimization variant — assign each word a value/cost and maximize/minimize over valid splits, the same way Weighted Event Scheduling extends plain Event Scheduling.

References / Links