Skip to content
DSA dynamic programming 8 min read

Edit Distance (Levenshtein): Dynamic Programming on Two Strings

Edit distance (also called Levenshtein distance) is the minimum number of single-character edits — insert, delete, or replace — needed to turn one string into another. Turning "horse" into "ros" takes 3 edits. It powers spell-checkers, fuzzy search, and diff, and it’s a beautiful 2D DP closely related to Longest Common Subsequence.

Defining the DP

Compare prefixes of the two strings. The subproblem is “how many edits to turn the first i characters of a into the first j characters of b?”

  • State: dp[i][j] = edit distance between a[0..i-1] and b[0..j-1].
  • Transition:
    • if a[i-1] == b[j-1]: no edit needed → dp[i][j] = dp[i-1][j-1].
    • else take the cheapest of three operations, plus 1:
      • replace: dp[i-1][j-1]
      • delete from a: dp[i-1][j]
      • insert into a: dp[i][j-1]
  • Base cases: dp[i][0] = i (delete all i chars), dp[0][j] = j (insert all j chars).

The answer is dp[m][n].

Tabulation: O(mn) time, O(mn) space

int editDistance(const std::string& a, const std::string& b) {
    int m = a.size(), n = b.size();
    std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
    for (int i = 0; i <= m; i++) dp[i][0] = i;       // base: delete all
    for (int j = 0; j <= n; j++) dp[0][j] = j;       // base: insert all
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            if (a[i - 1] == b[j - 1])
                dp[i][j] = dp[i - 1][j - 1];
            else
                dp[i][j] = 1 + std::min({ dp[i - 1][j - 1],  // replace
                                          dp[i - 1][j],      // delete
                                          dp[i][j - 1] });   // insert
    return dp[m][n];
}

Reading the three moves

Each transition maps to a concrete edit on a to make it match b:

  • dp[i-1][j-1] — characters already match (free) or are replaced.
  • dp[i-1][j]delete a[i-1], then solve the rest.
  • dp[i][j-1]insert b[j-1], then solve the rest.

For beginners: The base cases carry the whole logic for empty prefixes. To turn "" into "abc" you must insert 3 characters, so dp[0][3] = 3. Forgetting to initialize that first row/column is the most common bug.

Space optimization

Each cell depends only on the current and previous row, so you can keep two rows (or even one, with care) to drop space to O(min(m, n)). Keep the full grid only if you need to reconstruct the actual sequence of edits.

int editDistance(const std::string& a, const std::string& b) {
    int m = a.size(), n = b.size();
    std::vector<int> prev(n + 1), cur(n + 1);
    for (int j = 0; j <= n; j++) prev[j] = j;
    for (int i = 1; i <= m; i++) {
        cur[0] = i;
        for (int j = 1; j <= n; j++)
            cur[j] = (a[i - 1] == b[j - 1]) ? prev[j - 1]
                     : 1 + std::min({ prev[j - 1], prev[j], cur[j - 1] });
        std::swap(prev, cur);
    }
    return prev[n];
}

Complexity

ApproachTimeSpace
2D DPO(mn)O(mn)
Rolling rowsO(mn)O(min(m, n))

Pitfall: When a[i-1] == b[j-1], take dp[i-1][j-1] directly — do not add 1. Adding 1 on a match is the classic error that inflates every answer.

Edit distance and LCS are siblings; both generalize to the subsequence & string DP patterns. Next, see two-dimensional movement DP in DP on Grids.

Last updated June 25, 2026
Was this helpful?