LCS

두 수열에서 순서를 유지하면서 양쪽에 공통으로 등장하는 가장 긴 부분 수열(Longest Common Subsequence)을 찾는 문제입니다.

연속이어야 하는 부분 문자열(substring)과 달리, 부분 수열(subsequence)은 중간을 건너뛸 수 있습니다. ACAYKP와 CAPCAK의 LCS는 ACAK로 길이 4입니다.

동작 과정

dp[i][j] = A의 앞 i글자와 B의 앞 j글자의 LCS 길이.

A = "ACAYKP", B = "CAPCAK"

        ""  C  A  P  C  A  K
    "" [ 0  0  0  0  0  0  0 ]
    A  [ 0  0  1  1  1  1  1 ]
    C  [ 0  1  1  1  2  2  2 ]
    A  [ 0  1  2  2  2  3  3 ]
    Y  [ 0  1  2  2  2  3  3 ]
    K  [ 0  1  2  2  2  3  4 ]
    P  [ 0  1  2  3  3  3  4 ]
                            ↑ 답 = 4

칸을 채우는 규칙:
  A[i] == B[j]  → dp[i][j] = dp[i-1][j-1] + 1      (대각선 + 1)
  A[i] != B[j]  → dp[i][j] = max(dp[i-1][j], dp[i][j-1])  (위 또는 왼쪽 중 큰 값)

예: i=3(A), j=5(A) → 문자 일치
    dp[3][5] = dp[2][4] + 1 = 2 + 1 = 3

예: i=4(Y), j=5(A) → 불일치
    dp[4][5] = max(dp[3][5], dp[4][4]) = max(3, 2) = 3

문자가 같으면 양쪽에서 한 글자씩 소비해 LCS를 1 늘리고, 다르면 한쪽만 한 글자 포기했을 때의 더 좋은 쪽을 가져옵니다.

구현

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

    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] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[n][m];
}
// LCS 문자열 자체를 복원 — 표의 오른쪽 아래에서 거꾸로 추적
String reconstruct(String a, String b, int[][] dp) {
    StringBuilder sb = new StringBuilder();
    int i = a.length(), j = b.length();

    while (i > 0 && j > 0) {
        if (a.charAt(i - 1) == b.charAt(j - 1)) {
            sb.append(a.charAt(i - 1));   // 대각선에서 왔다 = LCS에 포함
            i--; j--;
        } else if (dp[i - 1][j] >= dp[i][j - 1]) {
            i--;                          // 위에서 왔다
        } else {
            j--;                          // 왼쪽에서 왔다
        }
    }
    return sb.reverse().toString();
}

복잡도

구분복잡도근거
시간O(NM)표의 모든 칸을 한 번씩 채움
공간O(NM)2차원 표. 길이만 필요하면 두 행만 번갈아 써서 O(min(N,M))으로 줄일 수 있다

주의사항

핵심 정리

→ 동적 계획법

CSLOG에서 보기