편집 거리

한 문자열을 다른 문자열로 바꾸는 데 필요한 삽입·삭제·교체 연산의 최소 횟수를 구하는 문제입니다. 레벤슈타인 거리(Levenshtein distance)라고도 합니다.

동작 과정

dp[i][j] = A의 앞 i글자를 B의 앞 j글자로 바꾸는 최소 연산 수.

A = "sunday", B = "saturday"

        ""  s  a  t  u  r  d  a  y
    "" [ 0  1  2  3  4  5  6  7  8 ]
    s  [ 1  0  1  2  3  4  5  6  7 ]
    u  [ 2  1  1  2  2  3  4  5  6 ]
    n  [ 3  2  2  2  3  3  4  5  6 ]
    d  [ 4  3  3  3  3  4  3  4  5 ]
    a  [ 5  4  3  4  4  4  4  3  4 ]
    y  [ 6  5  4  4  5  5  5  4  3 ]
                                  ↑ 답 = 3

첫 행/열은 "빈 문자열로 만들기" = 전부 삭제/삽입
  dp[i][0] = i,  dp[0][j] = j

칸을 채우는 규칙:
  A[i] == B[j] → dp[i][j] = dp[i-1][j-1]              (그대로 두기, 비용 0)
  A[i] != B[j] → dp[i][j] = 1 + min(
                     dp[i-1][j-1],   교체
                     dp[i][j-1],     삽입
                     dp[i-1][j])     삭제

예: i=3(n), j=3(t) → 불일치
    1 + min(dp[2][2]=1, dp[3][2]=2, dp[2][3]=2) = 1 + 1 = 2

sunday → saturday의 실제 연산 3개: a 삽입, t 삽입, n→r 교체.

구현

int editDistance(String a, String b) {
    int n = a.length(), m = b.length();
    int[][] dp = new int[n + 1][m + 1];

    for (int i = 0; i <= n; i++) dp[i][0] = i;   // 전부 삭제
    for (int j = 0; j <= m; j++) dp[0][j] = j;   // 전부 삽입

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a.charAt(i - 1) == b.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = 1 + Math.min(dp[i - 1][j - 1],              // 교체
                                Math.min(dp[i][j - 1], dp[i - 1][j])); // 삽입, 삭제
            }
        }
    }
    return dp[n][m];
}
// 공간 O(M) — 직전 행만 필요하다
int editDistanceOptimized(String a, String b) {
    int m = b.length();
    int[] prev = new int[m + 1];
    int[] cur = new int[m + 1];

    for (int j = 0; j <= m; j++) prev[j] = j;

    for (int i = 1; i <= a.length(); i++) {
        cur[0] = i;
        for (int j = 1; j <= m; j++) {
            if (a.charAt(i - 1) == b.charAt(j - 1)) {
                cur[j] = prev[j - 1];
            } else {
                cur[j] = 1 + Math.min(prev[j - 1], Math.min(cur[j - 1], prev[j]));
            }
        }
        int[] tmp = prev; prev = cur; cur = tmp;   // 두 행을 번갈아 재사용
    }
    return prev[m];
}

복잡도

구분복잡도근거
시간O(NM)표의 모든 칸을 한 번씩 채움
공간O(NM) → O(min(N,M))직전 행만 있으면 되므로 두 행만 유지 가능

주의사항

핵심 정리

→ 동적 계획법

CSLOG에서 보기