Start learning free

LESSON 452 OF 536 · DYNAMIC PROGRAMMING

Edit Distance in JavaScript: O(m · n) Dynamic Programming

Practice O(m · n) Read the full explanation

You are given two strings word1 and word2. Return the fewest operations that turn word1 into word2.

There are three operations, and each one counts as one:

  • Insert a character anywhere in the word.
  • Delete a character.
  • Replace a character with another one.

 

Example 1:

Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (delete 'r')
rose -> ros (delete 'e')
Two operations are not enough: "ros" is two characters shorter, so both would have to be deletes, and deleting letters from "horse" never gives "ros", because its 'o' comes before its 'r'.

Example 2:

Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (delete 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')

Example 3:

Input: word1 = "", word2 = "abc"
Output: 3
Explanation: The empty word becomes "abc" by inserting its 3 characters. Each operation adds at most one character, so no fewer will do.

 

Constraints:

  • 0 <= word1.length, word2.length <= 500
  • word1 and word2 are made of lowercase English letters only, a to z. Either of them can be empty.

Note:

Your algorithm should run in O(m · n) time and space, where m is the length of word1 and n the length of word2.



Trying every sequence of operations is far too slow. Solve smaller versions of the problem instead: let dp[i][j] be the fewest operations that turn the first i characters of word1 into the first j characters of word2. The table has (m + 1) × (n + 1) entries, and the answer is dp[m][n].
Row 0 and column 0 stand for an empty prefix. Turning i characters into nothing takes i deletes, so dp[i][0] = i; turning nothing into j characters takes j inserts, so dp[0][j] = j.

Fill the table row by row, so the entries above, to the left and diagonally up-left of dp[i][j] are already known. Look at the last characters of the two prefixes, word1[i - 1] and word2[j - 1]:
  • When they are equal, they need no operation: dp[i][j] = dp[i - 1][j - 1]. Spending an operation on either of them is never better.
  • When they differ, one operation fixes the end of the prefix, and dp[i][j] is 1 plus the smallest of three neighbours:
    • dp[i - 1][j - 1]: replace word1[i - 1] with word2[j - 1]; both prefixes are then one character shorter.
    • dp[i - 1][j]: delete word1[i - 1]; only the prefix of word1 is shorter.
    • dp[i][j - 1]: insert word2[j - 1] at the end; it matches the last character of word2's prefix, which is then one shorter.

The table has (m + 1) × (n + 1) entries, and each one takes a constant amount of work: O(m · n) time and O(m · n) space.
Each row reads only itself and the row above it, so two rows of n + 1 numbers are enough, which brings the space down to O(n). The lesson keeps the whole table, which is easier to follow.
STEP 1 OF 11
    Ctrl + Enter to check

    JAVASCRIPT · DYNAMIC PROGRAMMING

    Edit Distance in JavaScript, explained

    Problem overview

    The method minDistance(word1, word2) receives two strings and returns the fewest operations that turn word1 into word2. There are three operations and each costs one: insert a character anywhere, delete a character, or replace a character with another one.

    For word1 = "horse" and word2 = "ros" the answer is 3: replace h with r to get rorse, delete the second r to get rose, delete e to get ros. For "intention" and "execution" the answer is 5. For an empty word1 and "abc" it is 3, three inserts. Either word can be empty.

    Approach: solve every pair of prefixes

    Trying every sequence of operations is far too slow. The lesson solves smaller versions of the problem instead. Let dp[i][j] be the fewest operations that turn the first i characters of word1 into the first j characters of word2. The table has (m + 1) × (n + 1) entries, where m and n are the word lengths, and the answer is dp[m][n].

    Row 0 and column 0 stand for an empty prefix. Turning i characters into nothing takes i deletes, so dp[i][0] = i. Turning nothing into j characters takes j inserts, so dp[0][j] = j.

    For every other entry, look at the last characters of the two prefixes, word1[i - 1] and word2[j - 1]. The prefix of length i ends at index i - 1, because strings count from 0 while the table counts characters.

    • They are equal. They need no operation, so dp[i][j] = dp[i - 1][j - 1]. Spending an operation on either of them is never better.
    • They differ. One operation fixes the end, and dp[i][j] is 1 plus the smallest of three neighbours:
      • dp[i - 1][j - 1] for a replace: both prefixes then lose their last character;
      • dp[i - 1][j] for a delete of word1's last character: only the prefix of word1 gets shorter;
      • dp[i][j - 1] for an insert of word2's last character at the end: it matches the end of word2's prefix, which is then one shorter.

    Filling the table row by row and left to right means the three neighbours (above, left and up-left) are always filled before they are needed.

    For "horse" and "ros" the finished table is below. Rows are the prefixes of horse (the empty prefix, then h, ho, and so on) and columns are the prefixes of ros:

    0 1 2 3
    1 1 2 3
    2 2 1 2
    3 2 2 2
    4 3 3 2
    5 4 4 3
    

    The bottom-right entry, 3, is the answer.

    Step by step

    1. Store both lengths in constants, m for word1 and n for word2. They come up often.
    2. Create the table dp: m + 1 rows of n + 1 zeros, with Array.from({ length: m + 1 }, ...). The function passed to Array.from runs once per row, so each row is its own array.
    3. Fill column 0 with a loop for i from 0 to m, setting dp[i][0] to i: that many deletes.
    4. Fill row 0 with a loop for j from 0 to n, setting dp[0][j] to j: that many inserts. The entry dp[0][0] is set twice, both times to 0.
    5. Loop over the rest of the table. The outer loop runs i from 1 to m, and the inner one runs j from 1 to n, because row 0 and column 0 are already done.
    6. Handle equal last characters with if: copy dp[i - 1][j - 1].
    7. Handle different last characters in the else: take 1 + Math.min(...) of the three neighbours. This is the key line.
    8. Return dp[m][n] after the loops.

    The solution in JavaScript

    class Solution {
        minDistance(word1, word2) {
            const m = word1.length;
            const n = word2.length;
            const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
            for (let i = 0; i <= m; i++) {
                dp[i][0] = i;
            }
            for (let j = 0; j <= n; j++) {
                dp[0][j] = j;
            }
            for (let i = 1; i <= m; i++) {
                for (let j = 1; j <= n; j++) {
                    if (word1[i - 1] === word2[j - 1]) {
                        dp[i][j] = dp[i - 1][j - 1];
                    } else {
                        dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
                    }
                }
            }
            return dp[m][n];
        }
    }
    

    The JavaScript details worth knowing:

    • Building a two-dimensional array. Array.from({ length: m + 1 }, fn) treats the object as an array-like of length m + 1 and calls fn once per index, keeping what it returns. Every call makes a fresh new Array(n + 1).fill(0).
    • The shared-row trap. new Array(m + 1).fill(new Array(n + 1).fill(0)) looks equivalent but puts the same row object in every slot. Setting dp[0][1] = 9 on a table built that way changes every row: it gives [[0,9,0],[0,9,0]] for two rows of three.
    • Reading a character. word1[i - 1] gives a one-character string, and === compares two such strings by value. For an empty word the loops simply don't run.
    • Math.min with several arguments. It accepts any number of values, so the three neighbours go in directly.
    • Length versus index. The table index i counts characters, while the string index of the last one is i - 1. Mixing them up is the most common bug in this kind of table.

    Complexity

    Time: O(m · n). The table has (m + 1) × (n + 1) entries, and each one takes a constant amount of work.

    Space: O(m · n). The lesson keeps the whole table because it is easier to follow. Each row reads only itself and the row above it, so two rows of n + 1 numbers would be enough and would bring the space down to O(n).

    Edge cases and common mistakes

    • Empty words. minDistance("", "abc") returns 3, minDistance("abc", "") returns 3 and minDistance("", "") returns 0. The border loops handle all three without special code.
    • Equal words. minDistance("abc", "abc") returns 0, since every comparison takes the diagonal.
    • Single characters. minDistance("a", "b") returns 1, one replace.
    • Forgetting the + 1. Without it, a mismatch would cost nothing, and the table would never grow.
    • Table one entry too small. A table of m by n has no room for the empty prefixes. That is why both dimensions have the extra + 1.
    • Comparing word1[i] with word2[j]. The prefix of length i ends at index i - 1, so this compares the wrong characters and can read past the end of a word.
    • Treating replace as free. A replace costs 1, like an insert or a delete. Only equal characters are free.

    Testing

    • "horse" and "ros" give 3.
    • "intention" and "execution" give 5.
    • "" and "abc" give 3.
    • "abc" and "abc" give 0.
    • "a" and "b" give 1.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free