Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
A 'common child' of two strings is a string that is a subsequence of both — so the answer is the length of the Longest Common Subsequence (LCS). Build a 2D table dp where dp[i][j] is the LCS length of the first i characters of s1 and the first j of s2. When the current characters match, dp[i][j] = dp[i-1][j-1] + 1; otherwise dp[i][j] = max(dp[i-1][j], dp[i][j-1]). The answer is dp[m][n]. O(m·n) time, O(m·n) space (reducible to O(min(m,n))).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you recognise 'common child' as a thin disguise for Longest Common Subsequence, set up the 2D recurrence correctly, and know subsequence (gaps allowed) differs from substring (contiguous).
Where candidates slip
Solving Longest Common Substring instead (resetting to 0 on mismatch), or adding 1 on a mismatch, or confusing the diagonal/up/left transitions.
How to stand out
Derive the recurrence from the recursion tree, note the overlapping subproblems that justify DP, then mention the O(min(m,n)) two-row space optimisation and how backtracking would recover the actual child string.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewCompare s1[i-1] vs s2[j-1]Equal?YesNoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 3
Brute Force — Plain Recursion (Exponential)
1Hint 1Explore hintHide hint
Compare the last character of each prefix. If they're equal, that character can extend the common child — recurse on both prefixes minus their last char and add 1.
2Hint 2Explore hintHide hint
If the last characters differ, at least one of them isn't used as the final matched character. Try dropping the last char of s1, then dropping the last char of s2, and keep the larger result.
3Hint 3Explore hintHide hint
Base case: if either prefix is empty, the LCS is 0. This recursion is correct but recomputes the same (i, j) pair exponentially many times.
A · Core idea
Define lcs(i, j) = length of the longest common child of the first i characters of s1 and the first j characters of s2.
Match case. If s1[i-1] == s2[j-1], the two prefixes end in the same letter. That letter can always be the last character of an optimal common child of these prefixes, so lcs(i, j) = lcs(i-1, j-1) + 1. We never need to consider not using a matched ending character — using it is never worse.
Mismatch case. If the last characters differ, they cannot both be the final matched letter (they're different letters), so at least one of them is not the ending of the optimal child. We don't know which, so we try both: lcs(i-1, j) (the optimal child doesn't end with s1's last char) and lcs(i, j-1) (it doesn't end with s2's last char), and take the maximum.
Base case. An empty prefix shares nothing, so lcs(0, j) = lcs(i, 0) = 0.
Why it's too slow. On a full mismatch chain every call spawns two children, producing an exponential tree. But notice there are only (m+1)(n+1) distinct (i, j) pairs — the tree revisits them enormously. That overlap is the signal to memoise, which turns the recursion into the table below.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s1 = "AB", s2 = "AC"Interactive diagram
Recursion tree shows repeated subproblems
lcs(2,2)lcs(1,1) is reached by more than one path — the overlap that DP eliminates.
1C · Code with teaching comments
Reference implementation previewpublic int commonChild(String s1, String s2) { return lcs(s1, s2, s1.length(), s2.length()); } private int lcs(String s1, String s2, int i, int j) {Explore the full codeHide the full code
D · Why it works and complexityO(2^(m+n)) timeO(m+n) recursion stack spaceOpen the invariant and proofExplore the proofHide the proof
The recurrence has only two shapes — diagonal+1 on a match, max of up/left on a mismatch. Recognising that there are merely (m+1)(n+1) distinct states is what tells you this exponential recursion collapses into a polynomial DP.
ComplexityO(2^(m+n)) time, O(m+n) recursion stack space — Each mismatch branches into two recursive calls, so the recursion tree has up to 2^(m+n) leaves. The same (i, j) subproblem is solved over and over — that redundancy is exactly what DP removes.
E · Edge cases and pitfalls3Adding 1 on a mismatch — only a match contributes a character.Explore edge casesHide edge cases
Pitfalls
- Adding 1 on a mismatch — only a match contributes a character.
- Recursing without memoisation at |s| = 5000 — exponential, times out instantly.
- Treating it as a substring problem (resetting on mismatch) — solves the wrong question.
Approach 2 of 3 · Optimal
Bottom-Up 2D Table (Optimal)
1Hint 1Explore hintHide hint
Memoise the recursion: there are only (m+1)(n+1) distinct (i, j) states, so build a table dp[i][j] instead of recomputing.
2Hint 2Explore hintHide hint
Add a row and column of zeros (the empty-prefix base case) so you never index out of bounds and never special-case i==0 or j==0.
3Hint 3Explore hintHide hint
Fill row by row: dp[i][j] = dp[i-1][j-1] + 1 on a match, else max(dp[i-1][j], dp[i][j-1]). The answer is dp[m][n].
A · Core idea
Turn the recurrence into a table. Let dp[i][j] be the LCS length of s1[0..i) and s2[0..j). Row 0 and column 0 are all zeros — those encode the empty-prefix base case.
Filling a cell. For i, j ≥ 1, look at the two current characters s1[i-1] and s2[j-1]:
- If they're equal, the best common child of these prefixes is the best child of the shorter prefixes plus this shared letter:
dp[i][j] = dp[i-1][j-1] + 1. - If they differ, the optimal child can't end with both, so drop one and take the better:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
Each cell depends only on its diagonal, top, and left neighbours, all filled earlier in a row-major sweep, so a single pass top-to-bottom, left-to-right is enough.
Why the table is correct and minimal. The recursion above is provably correct; memoising it can't change the answer, only the running time. Because every cell is computed exactly once from O(1) neighbours, the whole table is O(m·n) — comfortably fast for the 5000×5000 worst case (~25M cells).
Space optimisation. Cell dp[i][j] only ever reads row i and row i-1. So you can keep just two rows (or even one row plus a saved diagonal), dropping memory from O(m·n) to O(min(m,n)). State that as the follow-up — it's the difference between 100 MB and a few KB at the size limit.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s1 = "HARRY", s2 = "SALLY"Array state
after 'A' row
dp[1][1]=1 (A==A); dp[1][2]=1 (carry)
2C · Code with teaching comments
D · Why it works and complexityO(m·n) timeO(m·n) spaceOpen the invariant and proofExplore the proofHide the proof
Each cell reads only its diagonal, top, and left neighbour. That locality is what makes the row-major fill valid in one pass and what enables the two-row O(min(m,n)) space trick: you never look further back than the previous row.
ComplexityO(m·n) time, O(m·n) space — Exactly (m+1)(n+1) cells, each computed in O(1) from three already-known neighbours → O(m·n) time. The full table is O(m·n) space; keeping only the previous and current rows reduces it to O(min(m,n)).
E · Edge cases and pitfalls7s1 = "AA", s2 = "BB" — No matches; every cell takes max of zeros → dp[2][2] = 0.Explore edge casesHide edge cases
Edge cases handled
s1 = "AA", s2 = "BB"No matches; every cell takes max of zeros → dp[2][2] = 0.s1 = "ABC", s2 = "ABC"Identical strings → matches all along the diagonal → dp[3][3] = 3.s1 = "A", s2 = "A"Single match → dp[1][1] = 1.s1 = "ABCDEF", s2 = "FBDAMN"Matches B then D in order → dp[m][n] = 2 ("BD").
Pitfalls
- Resetting dp[i][j] to 0 on a mismatch — that solves Longest Common *Substring*, not subsequence.
- Allocating a 5001×5001 int table in a language with tight memory — use the two-row optimisation if needed.
- Off-by-one: dp indices are 1-based but string indices are 0-based — always read s1[i-1], s2[j-1].
Approach 3 of 3
Two-Row Space Optimisation
1Hint 1Explore hintHide hint
Notice dp[i][j] only reads the current row i and the previous row i-1 — rows older than i-1 are never touched again.
2Hint 2Explore hintHide hint
Keep just two 1D arrays, prev and curr, of length n+1. After finishing row i, copy curr into prev (or swap the references).
3Hint 3Explore hintHide hint
Iterate the outer loop over the longer string and size the rows by the shorter string to make the arrays as small as possible.
A · Core idea
The full 2D table wastes memory: every cell only depends on the row above it and the cell to its left in the current row. So we keep two rolling arrays.
prev holds row i-1; curr is the row i we're building. For each j: a match uses prev[j-1] + 1 (the diagonal), a mismatch uses max(prev[j], curr[j-1]) (up and left). After completing a row, swap curr into prev and reuse the buffer.
Why it's identical in answer. We never read any value older than the previous row, so discarding earlier rows is information-lossless. The numbers computed are exactly the same as the full table; only storage changes.
When it matters. At |s1| = |s2| = 5000, the full int[5001][5001] table is ~100 MB. Two rows are ~40 KB. If a judge or interviewer flags memory, this is the fix — and looping the outer dimension over the longer string keeps the rows as short as possible, giving O(min(m,n)).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s1 = "AB", s2 = "AC"Array state
row 'A'
curr filled from prev=[0,0,0]
1C · Code with teaching comments
Reference implementation previewpublic int commonChild(String s1, String s2) { if (s1.length() < s2.length()) { String t = s1; s1 = s2; s2 = t; } int m = s1.length(), n = s2.length(); int[] prev = new int[n + 1]; int[] curr = new int[n + 1];Explore the full codeHide the full code
D · Why it works and complexityO(m·n) timeO(min(m,n)) spaceOpen the invariant and proofExplore the proofHide the proof
Rolling two rows is the standard space cut for any grid DP whose recurrence reaches back exactly one row. The moment you confirm 'no cell reads further than row i-1', you've earned the right to throw away everything older.
ComplexityO(m·n) time, O(min(m,n)) space — Same (m·n) cell computations, but at any moment only two rows of length n+1 exist → O(min(m,n)) space when you loop over the longer dimension.
E · Edge cases and pitfalls6s1 = "AA", s2 = "BB" — All mismatches → both rows stay zero → returns 0.Explore edge casesHide edge cases
Edge cases handled
s1 = "AA", s2 = "BB"All mismatches → both rows stay zero → returns 0.s1 = "ABCDE", s2 = "ACE"Matches A, C, E in order → returns 3 with rows of length 4.single char stringsOne row iteration; returns 1 if equal else 0.
Pitfalls
- Forgetting to reset/overwrite curr[0] — it must stay 0 (the empty-prefix column).
- Reading the wrong buffer after the swap — the answer is in prev[n] (post-swap), not curr[n].
- Not reusing the buffer (allocating a new row each iteration) — correct but defeats the memory goal.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int commonChild(String s1, String s2) { int m = s1.length(), n = s2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Substring instead of subsequence
Resetting dp[i][j] to 0 on a mismatch computes the longest *contiguous* common run. "HARRY"/"SALLY" then returns 1 (just one matching letter run) instead of 2 for the subsequence "AY".
Explore the code comparisonHide the code comparison
Substring instead of subsequence
Resetting dp[i][j] to 0 on a mismatch computes the longest *contiguous* common run. "HARRY"/"SALLY" then returns 1 (just one matching letter run) instead of 2 for the subsequence "AY".
Don't write this
if (s1.charAt(i-1) == s2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = 0; // longest common SUBSTRING, wrong problemWrite this instead
if (s1.charAt(i-1) == s2.charAt(j-1)) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);2Adding 1 on a mismatch
Only a matched character contributes length. Adding 1 in the max branch overcounts and produces lengths longer than either string.
Explore the code comparisonHide the code comparison
Adding 1 on a mismatch
Only a matched character contributes length. Adding 1 in the max branch overcounts and produces lengths longer than either string.
Don't write this
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + 1 # bug: +1 on mismatchWrite this instead
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])3No memoisation on the recursion
The plain recursion revisits each (i, j) exponentially. At |s| = 5000 it never finishes. Either memoise the recursion or write the bottom-up table.
Explore the code comparisonHide the code comparison
No memoisation on the recursion
The plain recursion revisits each (i, j) exponentially. At |s| = 5000 it never finishes. Either memoise the recursion or write the bottom-up table.
Don't write this
def lcs(i, j):
if i == 0 or j == 0: return 0
if s1[i-1] == s2[j-1]: return lcs(i-1, j-1) + 1
return max(lcs(i-1, j), lcs(i, j-1)) # recomputes everythingWrite this instead
from functools import lru_cache
@lru_cache(maxsize=None)
def lcs(i, j):
if i == 0 or j == 0: return 0
if s1[i-1] == s2[j-1]: return lcs(i-1, j-1) + 1
return max(lcs(i-1, j), lcs(i, j-1))Revise
Carry the pattern into the interview
Pattern to remember
Longest Common Subsequence — classic 2D table DP
Code pattern to memorize
dp[i][j] = LCS length of s1[0..i) and s2[0..j)
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
answer = dp[m][n]Rules to remember
- 1A 'child' is a subsequence (delete characters, keep order) — NOT a substring. Characters need not be contiguous.
- 2On a character match, you extend the diagonal:
dp[i-1][j-1] + 1. The +1 is the matched character. - 3On a mismatch, you drop one character from one string:
max(dp[i-1][j], dp[i][j-1]). Never add 1 on a mismatch. - 4Use 1-based indexing for the table with an extra row/column of zeros — it removes all the
i==0/j==0boundary checks. - 5You only need the length here, not the string itself, so the standard table suffices (no backtracking required).
Use this pattern when
- Two sequences, asked for the longest order-preserving common part
- Edit-distance / diff style problems (insert, delete, match)
- The words 'subsequence', 'common', 'not necessarily contiguous' appear
- You can frame progress as 'consume one character from one or both strings'
Choose another pattern when
- It must be **contiguous** → that's Longest Common Substring (reset to 0 on mismatch, track a global max)
- Only one string is involved → likely LIS or palind, not LCS
- Sequences are huge (10⁶+) and you need exact LCS → O(m·n) may be too slow; bit-parallel / Hunt–Szymanski needed
- You need the actual matched string, not its length → keep the table and backtrack from dp[m][n]
Interview takeaway
LCS is the canonical two-sequence grid DP. Match → take the diagonal plus one; mismatch → take the better of dropping a character from either string. Once you internalise this 2×2 cell rule you can solve Edit Distance, Shortest Common Supersequence, and diff tools with the same table.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternLongest Common Subsequence is the prototypical two-sequence grid DP. Match → diagonal + 1; mismatch → max of up and left. Recognise it whenever two sequences are compared for an order-preserving overlap, and remember the contiguous variant (Longest Common Substring) resets to 0 on a mismatch instead.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Longest Common SubsequenceThe exact same algorithm under its canonical name — Common Child IS LCS.
- Edit DistanceSame grid DP, but cells model insert/delete/replace costs instead of match/skip.
- Longest Palindromic SubsequenceLCS of the string with its own reverse gives the longest palindromic subsequence.
- Longest Common Subsequence (LeetCode)Identical table DP — practice the two-row space optimisation there.