Coding Interview Patterns Module 25 – Dynamic programming: knapsack, grids and strings
Edit distance with operations
The fewest insertions, deletions and replacements that turn one string into another, with the operations listed, from a typed city name to the official one.
What you will learn
- Compute the Levenshtein distance with insert, delete and replace
- Rebuild the list of operations in a fixed, testable order
- Prove the recurrence from the last column of an alignment
- Modify the costs for weighted edits, insertions and deletions only, or swapped letters
- Match typed names against a list, with a band of diagonals when only small distances matter
Before you start
What you’ll learn · Before you start
What you will learn
- Compute the Levenshtein distance with insert, delete and replace
- Rebuild the list of operations in a fixed, testable order
- Prove the recurrence from the last column of an alignment
- Modify the costs for weighted edits, insertions and deletions only, or swapped letters
- Match typed names against a list, with a band of diagonals when only small distances matter
Before you start
On this page
The edit distance between two strings is the smallest number of single-character edits, insertions, deletions and replacements, that turn one into the other; it is also called the Levenshtein distance. “Mysore” is 2 edits from “Mysuru” and “Hydrabad” 1 from “Hyderabad”, which is how a search box still finds the city someone meant. Interviews ask for the number first and then for the edits themselves. Both come from one table of (n + 1)(m + 1) cells, filled in O(nm) time for strings of n and m characters.
The problem: typed names against an official list
Problem A: the edit distance. Given strings a and b, return the fewest insertions, deletions and replacements
of single characters that turn a into b; each edit costs 1. Limits: 0 ≤ n, m ≤ 1,000.
"mysore"to"mysuru": 2, two replacements."vishakapatnam"to"visakhapatnam": 2, one deletion and one insertion.""to"agra": 4 insertions.
Problem B: the operations. Also return one cheapest list of edits, first to last, each with its position in the typed name.
An edit script lines the two names up letter by letter
Text description of the diagram
Each name is drawn as a row of letter boxes, with the typed name on top and the official name below it, so that the letters an edit script keeps sit one above the other, joined by a short line.
- Mysore over Mysuru: m, y, s and r are kept. The o above u and the e above u are marked R, two replacements, so the edit distance is 2.
- Vishakapatnam over Visakhapatnam: the h after "vis" in the typed name has an empty dashed box below it, marked D, because it is deleted; further right, an h of the official name has an empty dashed box above it, marked I, because it is inserted. Every other letter is kept, so the distance is again 2: one deletion and one insertion.
The brute force, and why it fails at these sizes
Look at the first character of each string. Try keeping or replacing it, deleting it from a and inserting the
character of b, solve what is left the same way, and take the cheapest of the three. Every call makes three more,
so the number of calls explodes: 13,483 for two names of 6 letters, 897,625 for names of 8 and 9 letters, about 2.1
billion for two of 13 letters and 1.45 × 10²² for two of 30. The answers are right; the time is hopeless long before
the limit of 1,000 characters.
# Edit distance by brute force: at every pair of positions, try deleting, inserting and replacing (or keeping an
# equal letter), and take the cheapest. Every call is counted, so the growth shows on short names.
calls = 0
def distance(a, b, i=0, j=0):
"""The fewest edits turning a[i:] into b[j:], trying every operation at every step."""
global calls
calls += 1
if i == len(a) or j == len(b):
return (len(a) - i) + (len(b) - j) # only insertions or only deletions are left
keep_or_replace = distance(a, b, i + 1, j + 1) + (a[i] != b[j])
delete = distance(a, b, i + 1, j) + 1
insert = distance(a, b, i, j + 1) + 1
return min(keep_or_replace, delete, insert)
def calls_needed(n, m):
"""How many calls the brute force makes on strings of n and m letters (the same for any letters)."""
c = [[1] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
c[i][j] = 1 + c[i - 1][j - 1] + c[i - 1][j] + c[i][j - 1]
return c[n][m]
for typed, city in [("Pune", "Poona"), ("Mysore", "Mysuru"), ("Kolkatta", "Kolkata"), ("Hydrabad", "Hyderabad")]:
calls = 0
d = distance(typed.lower(), city.lower())
print(f"{typed} -> {city}: distance {d}, {calls:,} calls")
print(f"Vishakapatnam -> Visakhapatnam (13 letters each) would make {calls_needed(13, 13):,} calls")
print(f"two strings of 30 letters: {calls_needed(30, 30):.2e} calls") Output
Pune -> Poona: distance 3, 1,021 calls Mysore -> Mysuru: distance 2, 13,483 calls Kolkatta -> Kolkata: distance 1, 162,817 calls Hydrabad -> Hyderabad: distance 1, 897,625 calls Vishakapatnam -> Visakhapatnam (13 letters each) would make 2,114,900,428 calls two strings of 30 letters: 1.45e+22 calls
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
What to notice
- Two numbers describe every call: how many characters of
aand ofbare already used. That is at most (n + 1)(m + 1) different calls, about a million for two strings of 1,000, while the brute force makes billions on names of 13 letters. - Each edit moves those numbers by one. A deletion uses a character of
a, an insertion one ofb, and a replacement or a kept character one of each.
Wagner and Fischer’s table stores the answer for every pair of prefixes. A walk back through the table lists the edits. The same table takes other costs: weighted edits, insertions and deletions only, or a swap of two neighbouring letters as one edit. When only small distances matter, a band of diagonals around the main one is enough.
The Wagner–Fischer table Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why the cheapest of three moves is right
- A dry run on mysore and mysuru
- Rebuilding the list of operations
- The code in five languages
- Changing the costs
- Fewer cells when the distance is small: a band of diagonals
- Swapped letters: optimal string alignment
- Matching a typed name against a list
- Edge cases and common bugs
- Exercise: the closest city
- The closest city to a typed name
- A practice set with sample answers
- Check yourself
- Interview questions
- Which edits does the Levenshtein distance count, and why is it never more than the length of the longer string?
- A delivery app must match a typed locality name against 1,000,000 official names in under 50 ms, allowing up to 2 typing slips. Comparing it with every name through the full table is far too slow. What do you build?
- Your function returns the edit operations, and a unit test compares them with a fixed list. After a refactor of the walk back the test fails, although the distance is unchanged and the new list is also correct. What happened, and how should the function and the tests change?
- A team counts a swap of two neighbouring letters as one edit and stores names in a BK-tree, which prunes its search with the triangle inequality. Some searches now miss names that are within the search radius. Explain why, and fix it.
- Key takeaways
1 diagram · 9 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 24 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- The String-to-String Correction Problem (R. A. Wagner and M. J. Fischer, Journal of the ACM 21(1), 1974) (Association for Computing Machinery)
- A technique for computer detection and correction of spelling errors (F. J. Damerau, Communications of the ACM 7(3), 1964) (Association for Computing Machinery)
- An Extension of the String-to-String Correction Problem (R. Lowrance and R. A. Wagner, Journal of the ACM 22(2), 1975) (Association for Computing Machinery)
- Some approaches to best-match file searching (W. A. Burkhard and R. M. Keller, Communications of the ACM 16(4), 1973) (Association for Computing Machinery)
- Algorithms for approximate string matching (E. Ukkonen, Information and Control 64, 1985) (Elsevier)
- A fast bit-vector algorithm for approximate string matching based on dynamic programming (G. Myers, Journal of the ACM 46(3), 1999) (Association for Computing Machinery)
- difflib.get_close_matches (The Python Standard Library) (Python Software Foundation)
- str.casefold (The Python Standard Library) (Python Software Foundation)
- The Java Language Specification, Java SE 25: 4.2.1 Integral types and values (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress