Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Decide whether string `a` can be turned into the all-uppercase string `b` using only two moves: capitalize zero or more lowercase letters of `a`, and delete the remaining lowercase letters. Uppercase letters in `a` are frozen — they can neither be deleted nor changed. This is a two-sequence subsequence-matching DP: dp[i][j] = can the first i characters of a produce the first j characters of b. A lowercase a[i-1] may be deleted (carry dp[i-1][j]) or capitalized to match b[j-1] (take dp[i-1][j-1]); an uppercase a[i-1] MUST match b[j-1] exactly. O(n·m) time and space.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you model an asymmetric transform (lowercase = delete-or-capitalize, uppercase = must-match) as a 2D boolean reachability DP rather than greedy character matching.
Where candidates slip
Trying to match greedily left-to-right (capitalize whenever it fits) — this fails because sometimes a lowercase letter that *could* match must instead be deleted so a later letter can match. Also forgetting that an unmatched uppercase letter in `a` makes the whole thing impossible.
How to stand out
Name it as a subsequence/edit-distance-style grid, derive the two branches from the case of the character, handle the j=0 column explicitly, then note the O(m)-space rolling-row optimization.
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 previewAt a[i-1]Uppercase?YesNo (lowercase)Explore 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 — Recurse on Capitalize / Delete
1Hint 1Explore hintHide hint
Walk a pointer i over `a` and j over `b`. At each step, what can you do with a[i] depending on whether it's upper or lowercase?
2Hint 2Explore hintHide hint
If a[i] is lowercase you branch: either capitalize it (only legal when it uppercases to b[j], then advance both) or delete it (advance i only). If a[i] is uppercase it must equal b[j] to advance both, else this path fails.
3Hint 3Explore hintHide hint
Base cases: if j reached the end of b, the rest of a must be all lowercase (deletable). If i reached the end of a but j hasn't, fail.
A · Core idea
The most literal reading: try to consume b one character at a time while scanning a. Maintain two indices, i into a and j into b.
The branches. If a[i] is lowercase, you have a genuine choice. You may capitalize it — but only if toUpper(a[i]) == b[j] — which consumes b[j] and advances both pointers. Or you may delete it, advancing only i. If a[i] is uppercase, there is no choice: it must equal b[j] (advance both); if it doesn't, this path is impossible.
Why it must branch (greedy fails). Consider a = "Aa", b = "A". Greedily capitalizing the lowercase 'a' would over-produce; you must delete it instead. Conversely sometimes you must capitalize even though deleting looks simpler. Because a lowercase letter that could match might need to be deleted to let a later letter match, you cannot commit greedily — you explore both forks.
Base cases. When j == m (all of b produced), success requires every remaining character of a to be lowercase (so they can be deleted); a leftover uppercase letter forces failure. When i == n but j < m, you've run out of letters to supply b → failure.
Why we don't ship this. Every lowercase letter doubles the path count, so the tree is 2ⁿ. But the only thing that distinguishes a subproblem is the pair (i, j), and there are just n·m such pairs — the same states are recomputed exponentially often. That redundancy is the cue to memoize.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
a = "aB", b = "B"Interactive diagram
Recursion forks on every lowercase letter
f(0,0)The same (i, j) state can be reached on multiple paths — the exponential waste.
YESC · Code with teaching comments
Reference implementation previewpublic String abbreviation(String a, String b) { return solve(a, b, 0, 0) ? "YES" : "NO"; } private boolean solve(String a, String b, int i, int j) {Explore the full codeHide the full code
D · Why it works and complexityO(2ⁿ) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
There are only n·m distinct (i, j) states but exponentially many paths reach them. Caching each (i, j) the first time collapses 2ⁿ into n·m — the universal DP trigger of 'few states, recomputed often'.
ComplexityO(2ⁿ) time, O(n) space — Each lowercase character can branch two ways, giving up to 2ⁿ leaf paths; recursion depth is O(n) for the call stack.
E · Edge cases and pitfalls3Forgetting to verify leftover characters are lowercase when j hits the end of b.Explore edge casesHide edge cases
Pitfalls
- Forgetting to verify leftover characters are lowercase when j hits the end of b.
- Allowing the capitalize branch when toUpper(a[i]) != b[j] — an illegal move.
- Running it on |a| = 1000 — 2ⁿ never returns; memoize or tabulate.
Approach 2 of 3 · Optimal
2D Boolean DP — Tabulation (Optimal)
1Hint 1Explore hintHide hint
Define dp[i][j] = can the first i characters of a be transformed into the first j characters of b? You want dp[n][m].
2Hint 2Explore hintHide hint
dp[0][0] = true (empty produces empty). Fill the j=0 column: dp[i][0] is true only while a[0..i-1] is all lowercase (everything deletable).
3Hint 3Explore hintHide hint
Transition by the case of a[i-1]: uppercase forces dp[i][j] = (a[i-1]==b[j-1]) && dp[i-1][j-1]; lowercase allows delete (dp[i-1][j]) OR capitalize (dp[i-1][j-1] when upper(a[i-1])==b[j-1]).
A · Core idea
Turn the recursion inside-out. Let dp[i][j] mean 'the first `i` characters of `a` can be transformed into the first `j` characters of `b`'. The answer is dp[n][m].
Initialization. dp[0][0] = true — an empty prefix of a trivially yields an empty prefix of b. The first column dp[i][0] asks 'can a[0..i-1] be deleted entirely?'. That's true only if every one of those characters is lowercase, so dp[i][0] = dp[i-1][0] && isLowercase(a[i-1]). The first row dp[0][j>0] is false: nothing can produce a non-empty b.
Transition — uppercase a[i-1]. A frozen letter can only be kept, and only if it equals the target: dp[i][j] = (a[i-1] == b[j-1]) && dp[i-1][j-1]. If they differ, the cell is false.
Transition — lowercase a[i-1]. Two ways to reach dp[i][j]:
- Delete
a[i-1]: the target prefix is unchanged, so inheritdp[i-1][j]. - Capitalize
a[i-1]to consumeb[j-1]: legal only whentoUpper(a[i-1]) == b[j-1], contributingdp[i-1][j-1].
Combine with OR: dp[i][j] = dp[i-1][j] || (toUpper(a[i-1]) == b[j-1] && dp[i-1][j-1]).
Why iterate j from 0. The j == 0 column carries the 'delete everything so far' state. If you start j at 1 you lose the ability to delete a prefix of a before the first b character is matched — and a = "xY", b = "Y" would wrongly fail.
Why this is correct and fast. Every (i, j) state is computed exactly once from already-filled neighbours above and to the upper-left, so there's no recomputation. n·m ≤ 10⁶ cells for the constraints — instant.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
a = "daBcd", b = "ABC"How to read it:Delete steps down a row; capitalize/match steps diagonally consuming a b character.
YESC · Code with teaching comments
D · Why it works and complexityO(n·m) timeO(n·m) spaceOpen the invariant and proofExplore the proofHide the proof
The grid is identical in shape to Edit Distance and LCS; only the cell semantics change from 'cost' to 'reachable'. Recognising that lets you reuse the same loop skeleton and just swap the recurrence body — case-of-character decides the move set.
ComplexityO(n·m) time, O(n·m) space — Exactly (n+1)(m+1) cells, each filled in O(1) → O(n·m) time. The full table is O(n·m) space.
E · Edge cases and pitfalls7a = "Pi", b = "P" — Keep 'P', delete lowercase 'i' → dp[2][1] inherits dp[1][1]=T → YES.Explore edge casesHide edge cases
Edge cases handled
a = "Pi", b = "P"Keep 'P', delete lowercase 'i' → dp[2][1] inherits dp[1][1]=T → YES.a = "a", b = "A"Capitalize 'a'→'A': dp[1][1] from dp[0][0]=T → YES.a = "AB", b = "ABC"Only 2 frozen letters, need 3 targets; dp[2][3] stays false → NO.a = "Ab", b = "AB" then trailing 'C'An uppercase letter with no matching b position keeps its column false → NO when unmatched.
Pitfalls
- Starting the j loop at 1 — loses the delete-the-prefix state in column 0.
- Comparing a[i-1] == b[j-1] in the lowercase branch instead of toUpper(a[i-1]) == b[j-1].
- Forgetting that an uppercase letter that doesn't match simply leaves the cell false (never OR-in a delete for it).
Approach 3 of 3
Rolling-Row DP — O(m) Space
1Hint 1Explore hintHide hint
dp[i][*] depends only on dp[i-1][*]. Do you ever read a row older than the previous one?
2Hint 2Explore hintHide hint
Keep two 1D arrays, prev and cur, of length m+1. Compute cur from prev each row, then swap.
3Hint 3Explore hintHide hint
Watch the j=0 cell of cur: it must encode 'a[0..i-1] all lowercase', so set it before the inner loop based on prev[0] and the case of a[i-1].
A · Core idea
The tabulation only ever reads the immediately previous row, so storing all n+1 rows is wasteful. Keep prev (row i-1) and cur (row i).
Handling column 0 per row. cur[0] answers 'can a[0..i-1] be entirely deleted?', which is prev[0] && isLowercase(a[i-1]). Set this before the inner loop because the lowercase delete branch reads cur's own column-0 lineage indirectly through prev.
The inner loop is identical to the 2D version with dp[i-1][j] → prev[j] and dp[i-1][j-1] → prev[j-1]. After finishing the row, swap prev and cur (and clear cur).
Why ship it when |a| is large. With |a|, |b| ≤ 1000 the full table is only ~10⁶ booleans — fine. But the O(m) version generalises better and demonstrates you see the bounded look-back. The trade-off: you lose the full table, so you can't reconstruct which letters were capitalised vs deleted without extra bookkeeping.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
a = "AbcDE", b = "ABDE"How to read it:Only prev and cur exist at any time; cur becomes prev each iteration.
YESC · Code with teaching comments
Reference implementation previewpublic String abbreviation(String a, String b) { int n = a.length(), m = b.length(); boolean[] prev = new boolean[m + 1]; prev[0] = true; for (int i = 1; i <= n; i++) {Explore the full codeHide the full code
D · Why it works and complexityO(n·m) timeO(m) spaceOpen the invariant and proofExplore the proofHide the proof
A DP whose recurrence reads only the previous row collapses from O(n·m) to O(m) space by alternating two rows. The same trick shrinks LCS, Edit Distance, and 0/1 Knapsack — recognise the single-row look-back and the optimisation is mechanical.
ComplexityO(n·m) time, O(m) space — Still (n+1)(m+1) cell computations → O(n·m) time, but only two rows of length m+1 are retained → O(m) space.
E · Edge cases and pitfalls6a = "abc", b = "" (empty) — m=0; prev[0] stays true through all lowercase rows → YES (delete everything).Explore edge casesHide edge cases
Edge cases handled
a = "abc", b = "" (empty)m=0; prev[0] stays true through all lowercase rows → YES (delete everything).a = "Xabc", b = "" (empty)Row 'X' sets cur[0] = false (uppercase can't be deleted) → NO.a = "a"×1000, b = "A"×1000Diagonal of capitalizations fills cur[m] → YES; O(m) space handles the size.
Pitfalls
- Reusing a single array in place — reading already-overwritten cur[j-1] corrupts the row; use separate prev/cur.
- Forgetting to recompute cur[0] each row — it changes the moment an uppercase letter appears.
- Expecting to recover the capitalize/delete choices — O(m) space discards the witness.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic String abbreviation(String a, String b) { int n = a.length(), m = b.length(); boolean[][] dp = new boolean[n + 1][m + 1]; dp[0][0] = true; for (int i = 1; i <= n; i++) {Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Greedy capitalize instead of branching
On a = "Aa", b = "A": greedily capitalizing the lowercase 'a' produces "AA" ≠ "A". You must delete it. Only branching (or the DP's OR) captures this.
Explore the code comparisonHide the code comparison
Greedy capitalize instead of branching
On a = "Aa", b = "A": greedily capitalizing the lowercase 'a' produces "AA" ≠ "A". You must delete it. Only branching (or the DP's OR) captures this.
Don't write this
# greedy: capitalize whenever it matches
if ca.upper() == b[j]:
j += 1 # forced, no delete option consideredWrite this instead
dp[i][j] = dp[i - 1][j] # delete option
if ca.upper() == b[j - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j - 1] # capitalize option2Ignoring leftover uppercase letters
When b is exhausted, any remaining uppercase letter in a is undeletable, so the match fails. Returning true unconditionally at j==m is wrong.
Explore the code comparisonHide the code comparison
Ignoring leftover uppercase letters
When b is exhausted, any remaining uppercase letter in a is undeletable, so the match fails. Returning true unconditionally at j==m is wrong.
Don't write this
if (j == b.length()) return true; // ignores leftover uppercase in aWrite this instead
if (j == b.length()) {
for (int k = i; k < a.length(); k++)
if (Character.isUpperCase(a.charAt(k))) return false;
return true;
}3Comparing wrong case in lowercase branch
A lowercase letter matches b[j-1] only after capitalization, so you must compare toUpper(a[i-1]), not a[i-1] itself.
Explore the code comparisonHide the code comparison
Comparing wrong case in lowercase branch
A lowercase letter matches b[j-1] only after capitalization, so you must compare toUpper(a[i-1]), not a[i-1] itself.
Don't write this
if (ca == b.charAt(j - 1)) dp[i][j] |= dp[i-1][j-1]; // never true: ca is lowercaseWrite this instead
if (Character.toUpperCase(ca) == b.charAt(j - 1)) dp[i][j] |= dp[i-1][j-1];Revise
Carry the pattern into the interview
Pattern to remember
Two-sequence boolean subsequence-matching DP (edit-style grid)
Code pattern to memorize
dp[0][0] = true
for i in 1..n, j in 0..m:
if a[i-1] is UPPERCASE:
dp[i][j] = (j>0 && a[i-1]==b[j-1]) ? dp[i-1][j-1] : false
else # lowercase
dp[i][j] = dp[i-1][j] # delete a[i-1]
|| (j>0 && upper(a[i-1])==b[j-1] # OR capitalize it
&& dp[i-1][j-1])
answer = dp[n][m]Rules to remember
- 1
bis always all uppercase — every character ofbmust be produced by either keeping an uppercase letter ofaor capitalizing a lowercase one. - 2An uppercase letter in
ais non-negotiable: it cannot be deleted, so if it doesn't line up with the currentbcharacter the path dies. - 3A lowercase letter in
ais a free agent: delete it (move down a row, same column) or capitalize it to consume abcharacter (move diagonally) — only if it uppercases to that character. - 4Iterate
jfrom 0, not 1: thej == 0column answers 'can a[0..i-1] vanish entirely?', which is true only while every letter seen is lowercase. - 5Compare
Character.toUpperCase(a[i-1]) == b[j-1], nevera[i-1] == b[j-1]for the lowercase branch — you're matching the capitalized form.
Use this pattern when
- Two strings, asking 'can one transform into the other' under per-character keep/delete/transform rules
- One string is a constrained subsequence of the other after a case or filter operation
- Characters split into 'mutable' and 'frozen' classes with different allowed moves
- A yes/no reachability question over a 2D (prefix-of-a, prefix-of-b) state
Choose another pattern when
- You need the minimum number of edits, not a yes/no → Edit Distance (numeric DP)
- Both strings are freely editable with insert as well → classic LCS / edit-distance family
- Single string, no second target → 1D DP, not this grid
- Wildcards `?`/`*` rather than case rules → Wildcard Matching
Interview takeaway
Abbreviation is Edit Distance's boolean cousin: the same prefix-by-prefix grid, but each cell is reachable-or-not instead of a cost, and the allowed moves are dictated by the *case* of the current character. Split characters into mutable (lowercase) and frozen (uppercase) and the recurrence writes itself.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternTwo-sequence boolean reachability DP — Edit Distance's grid with reachable-or-not cells. The case of each character of `a` selects the move set: lowercase = delete or capitalize, uppercase = forced match. Recognise it whenever a transform question pits a 'mutable' character class against a 'frozen' one over two strings.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Edit DistanceSame prefix-by-prefix grid, but cells hold minimum cost (insert/delete/replace) instead of a boolean.
- Wildcard Matching2D boolean DP again, where '*' and '?' set the legal moves instead of letter case.
- Longest Common SubsequenceThe numeric ancestor of this grid — match consumes both indices, mismatch takes the better neighbour.
- Distinct SubsequencesComing soonCount (not yes/no) the ways one string forms another as a subsequence — the same grid with addition replacing OR.