Edit Distance
Minimum insert / delete / replace operations to turn word1 into word2.
Intuition
Same skeleton as LCS: compare prefix pairs. dp[i][j] = cost to convert word1[:i] into word2[:j]. Matching last characters cost nothing — inherit the diagonal. Otherwise the last edit must be one of exactly three moves (replace, delete, insert), each reducing to a neighboring smaller subproblem — take the cheapest and add 1.
Same grid as longest-common-subsequence, but minimising instead of maximising — and with a genuine base case rather than zeros. The three neighbours you consult are the three operations: diagonal is replace, up is delete, left is insert. Whenever a problem offers a fixed menu of moves that each consume a bit of the input, each move becomes one neighbour in the DP table.
Approach
Base cases are the frame
dp[i][0] = i (delete everything), dp[0][j] = j (insert everything).
Three moves, three neighbors
Mismatch at (i,j): replace → dp[i-1][j-1]+1, delete word1's char → dp[i-1][j]+1, insert word2's char → dp[i][j-1]+1. Match: dp[i-1][j-1] free.
Why this is complete
Any optimal edit script has some last operation touching the ends, and it must be one of these three (or a free match) — so the min over them is optimal. Classic Levenshtein distance.
Solution & live demo
Common pitfalls
Leaving the first row and column at zero
dp = [[0] * (n + 1) for _ in range(m + 1)] # straight into the loops
for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j
Turning a 5-character word into the empty string costs 5 deletions, not 0. Unlike LCS, the empty-prefix cases here are non-zero, and leaving them empty makes every distance too small.
Adding 1 on a character match
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]Matching characters need no edit at all — you advance both strings for free. This is exactly inverted from LCS, where a match is the only time the count grows, and copying that habit across is the usual slip.
Taking the min over only two neighbours
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1])
dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])
Dropping the diagonal removes replace from the menu, so a single substitution gets billed as a delete plus an insert. "cat" to "cut" then reports 2 instead of 1.
Edge cases
Answer is the other word's length (all inserts or all deletes).
Diagonal is free all the way — 0.
max(m,n): replace along the diagonal, insert/delete the remainder.