String · 14 of 43
EDIT Distance
Find the fewest single letter changes (add one, remove one, or swap one) that turn one word into another, using a chart of smaller answers.
- Simple way
- time O(3^(m+n))
room O(m + n) - Clever way
- time O(m * n)
room O(m * n)
The problem, simply
You get two words, word1 of length m and word2 of length n. You may change word1 one letter at a time, in three ways:
- insert a letter anywhere,
- delete a letter,
- replace a letter with another letter.
Give back the fewest changes needed to make word1 equal to word2.
A story to remember it
Picture it
The finished chart for horse and ros. Row labels are the letters of horse, column labels the letters of ros. The bottom right box, 3, is the answer.
- answer
Examples
A normal case
- In
word1 = 'horse', word2 = 'ros'- Out
3
Replace h with r (rorse), delete the r (rose), delete the e (ros).
Longer words
- In
word1 = 'intention', word2 = 'execution'- Out
5
Delete t, replace i with e, replace n with x, replace n with c, insert u.
Mostly the same
- In
word1 = 'sunday', word2 = 'saturday'- Out
3
Insert a and t after the s, then replace n with r.
Starting from nothing
- In
word1 = '', word2 = 'abc'- Out
3
Three inserts. The edges of the chart cover this.
Already equal
- In
word1 = 'same', word2 = 'same'- Out
0
No changes at all.
Way 1: the simple way
Compare the two words from the front with two fingers, i in word1 and j in word2.
- If
word1is used up, the rest ofword2must be inserted:n - jchanges. - If
word2is used up, the rest ofword1must be deleted:m - ichanges. - If the letters match, move both fingers, no cost.
- If not, try all three fixes and keep the cheapest, plus 1:
- delete
word1[i]: moveionly, - insert
word2[j]: movejonly, - replace: move both.
- delete
class Solution {
public int minDistance(String word1, String word2) {
return solve(word1, word2, 0, 0);
}
private int solve(String a, String b, int i, int j) {
if (i == a.length()) return b.length() - j; // insert the rest
if (j == b.length()) return a.length() - i; // delete the rest
if (a.charAt(i) == b.charAt(j)) return solve(a, b, i + 1, j + 1);
int delete = solve(a, b, i + 1, j);
int insert = solve(a, b, i, j + 1);
int replace = solve(a, b, i + 1, j + 1);
return 1 + Math.min(delete, Math.min(insert, replace));
}
}How long it takes: every mismatch splits into three, and the depth can reach m + n, so O(3^(m+n)). The same (i, j) question is asked again and again. Extra room: the call stack, O(m + n).
Way 2: the clever way
There are only (m + 1) * (n + 1) different questions. Answer each one once and write it in a chart. Let box (i, j) be the fewest changes to turn the first i letters of word1 into the first j letters of word2.
- Top row:
(0, j) = j, because you insertjletters. Left column:(i, 0) = i, because you deleteiletters. - If letter
iofword1equals letterjofword2: copy the box up and left,(i - 1, j - 1). - Otherwise take 1 plus the smallest of three boxes:
- the box above,
(i - 1, j), means delete, - the box to the left,
(i, j - 1), means insert, - the box up and left,
(i - 1, j - 1), means replace.
- the box above,
- The answer is in the bottom right box.
Filling the chart for horse and ros
01 / 12
Box (i, j) will hold the fewest changes to turn the first i letters of horse into the first j letters of ros. The dash means no letters at all.
- looking now
- checking
- done
- answer
- path
- just added
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) dp[i][0] = i; // delete everything
for (int j = 0; j <= n; j++) dp[0][j] = j; // insert everything
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // same letter, free
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], // replace
Math.min(dp[i - 1][j], // delete
dp[i][j - 1])); // insert
}
}
}
return dp[m][n];
}
}How long it takes: (m + 1) * (n + 1) boxes, each in constant time, O(m * n). Extra room: the chart, O(m * n).
Watch out for
- Forgetting the edges. Without
dp[i][0] = ianddp[0][j] = j, empty words get cost 0 and every answer is too small. - When the letters match, do not also add 1 or take a minimum with the others. Copying the diagonal is always at least as good.
- Which box is insert and which is delete is easy to swap. It does not change the number, but it matters if you print the steps.
What interviewers ask next
- Can you use less room? Each row only needs the row above it. Keep two rows, or even one row plus a saved diagonal value, for
O(n)room. Put the shorter word on the columns forO(min(m, n)). - Print the actual changes? Start at the bottom right box and walk back: diagonal on a match, otherwise to whichever neighbor gave the minimum, writing replace, delete or insert as you go.
- What if the three changes have different costs? Use the costs instead of 1 in each choice. The chart works the same way.
- Only inserts and deletes allowed? Then the answer is
m + n - 2 * LCS, where LCS is the longest common subsequence. With replace allowed this formula is wrong, because one replace can do the work of a delete plus an insert. - Is the edit distance within 1 (LeetCode 161)? No chart needed. Walk both words once and allow one difference,
O(n).
Remember this
Grown-up words: the fewest changes is the edit distance, also called Levenshtein distance, the chart is a dynamic programming table, answering each question once is memoization or tabulation, and walking back through the chart is reconstructing the solution.