1048 - Longest String Chain
Difficulty: Medium | Pattern: Dynamic Programming | Company tags: Amazon, Facebook, Google
Problem Statement
You are given an array of words where each word consists of lowercase English letters.
wordA is a predecessor of wordB if and only if you can insert exactly one letter anywhere in wordA without changing the order of the other characters to make it equal to wordB.
- E.g.,
"abc"is a predecessor of"abac".
A word chain is a sequence of words where each word is a predecessor of the next.
Return the length of the longest possible word chain.
Example 1:
Input: words = ["a","b","ba","bca","bda","bdca"]
Output: 4
Explanation: "a","ba","bda","bdca"
Example 2:
Input: words = ["xbc","pcxbcf","xb","cxbc","pcxbc"]
Output: 5
Algorithm Flow
Approach: DP with Word Hashing — O(n × L²)
Key insight: Sort words by length. For each word, try removing one character at a time — if the resulting word exists in our DP map, it could be the predecessor. Track the longest chain ending at each word.
def longestStrChain(words: list[str]) -> int:
words.sort(key=len)
dp = {} # word → longest chain ending here
best = 1
for word in words:
dp[word] = 1 # at minimum, chain of length 1
for i in range(len(word)):
prev = word[:i] + word[i+1:] # remove character at index i
if prev in dp:
dp[word] = max(dp[word], dp[prev] + 1)
best = max(best, dp[word])
return best
Dry Run
words = ["a","b","ba","bca","bda","bdca"] (already sorted by length)
| word | predecessors tried | dp value |
|---|---|---|
| a | (none, len 1) | 1 |
| b | (none, len 1) | 1 |
| ba | "a"(exist,1), "b"(exist,1) | max(1+1,1+1)=2 |
| bca | "ca", "ba"(exist,2), "bc" | max(dp["ba"]+1)=3 |
| bda | "da", "ba"(exist,2), "bd" | max(dp["ba"]+1)=3 |
| bdca | "dca","bca"(exist,3),"bda"(exist,3),"bdc" | max(3+1,3+1)=4 |
best = 4 ✓ Chain: a→ba→bda→bdca
Edge Cases
- Single word → 1
- No predecessors → all dp values = 1, return 1
- All words form one long chain → return n
Complexity
- Time: O(n × L²) — for each of n words, try L removals, each O(L) for slicing
- Space: O(n × L) for dp map
Key Terms
| Term | Definition |
|---|---|
| Dynamic Programming (DP) | Building the answer to a large problem from cached answers to smaller subproblems, here dp[word] = longest chain ending at word. |
| Predecessor | A word that becomes the current word after inserting exactly one character. |
| Sort by length | Preprocessing step ensuring every possible predecessor is already processed (and in dp) before the current word is evaluated. |
| Hashing (dict lookup) | Using a hash map (dp) for O(1) average lookup of whether a candidate predecessor was already seen. |
FAQ
Q: Can this be solved without extra space?
A: No — the dp dictionary is essential to avoid recomputation; without memoization the approach degrades into exponential re-derivation of chains.
Q: What if the input array is empty or has one word?
A: An empty list returns best = 1 by default in most implementations (though the loop never runs, so guard for that); a single word returns 1, since dp[word] = 1 and no predecessors exist.
Q: Why must words be sorted by length before the main loop?
A: Because dp[prev] must already be computed when we look it up — sorting guarantees every shorter word is processed before any longer word that could depend on it.
Q: How would this change if insertion could add more than one character?
A: The predecessor generation (word[:i] + word[i+1:]) would need to remove multiple characters in combination, which is far more expensive — this problem's O(L) removal-per-position trick specifically relies on "exactly one" character difference.
Q: Iterative DP vs top-down memoized recursion — which is preferable here? A: Iterative (as shown) is simpler because sorting by length naturally establishes the correct processing order; a recursive version would need explicit memoization but avoids needing to pre-sort.
Quick Revision
- Goal: find the longest chain where each word is formed by inserting one letter into the previous word.
- Sort words by length so every possible predecessor is processed before its successors.
- For each word, try removing one character at each index to generate a candidate predecessor string.
- If a candidate exists in
dp,dp[word] = max(dp[word], dp[candidate] + 1). - Track the running maximum (
best) across all words. - Time: O(n × L²) — n words, L removal candidates, each O(L) to build via slicing.
- Space: O(n × L) for storing all words as dict keys.
- This is a variant of Longest Increasing Subsequence where "increasing" means "one insertion away" instead of "numerically greater."
Related Problems
- 300 - Longest Increasing Subsequence — same "chain-building DP over sorted input" pattern.
- Word Ladder / Word Ladder II (pattern: BFS over one-character-edit graphs) — not in this directory.
- Longest Common Subsequence (pattern: DP over string relationships) — not in this directory.