LCS (Longest Common Subsequence) — 점화식을 손으로 납득하기까지

DP 문제를 풀 때 점화식을 외우는 건 별로 좋아하지 않는다. 어차피 까먹는다. 그래서 LCS도 직접 납득하고 싶었다.


LCS가 뭔지부터

두 수열이 주어졌을 때, 양쪽에 공통으로 존재하는 부분 수열 중 가장 긴 것.
연속일 필요는 없고, 순서만 유지하면 된다.

A = "ABCDE"
B = "ACE"
→ LCS = "ACE" (길이 3)

dp 테이블 정의

dp[i][j] = A의 i번째, B의 j번째까지 고려했을 때의 LCS 길이

여기까지는 자연스럽게 나왔다.


종이에 매트릭스를 그리면서 막혔던 부분

2차원 테이블을 채우는 게 LCS의 전형적인 풀이라는 건 알고 있었다. 그래서 직접 그려봤는데, 두 가지가 와닿지 않았다.

첫째, A[i] == B[j]일 때 왼쪽 대각선 위 dp[i-1][j-1]에서 값을 가져온다는 게 기계적으로만 보였다. 왜 하필 대각선인지.

둘째, A[i] != B[j]일 때 왼쪽과 위 중 max를 취하는데, 대각선은 왜 고려 안 해도 되는지. 매트릭스를 채우다 보니 자연스럽게 드는 의문이었다.


납득한 과정

Case 1: A[i] == B[j]

두 문자가 같다면, 이걸 LCS의 마지막 문자로 쓰기로 결정한다.
그러면 나머지 LCS는 A의 i-1번째까지, B의 j-1번째까지에서 와야 한다.
두 문자를 둘 다 소비했으니까.

dp[i][j] = dp[i-1][j-1] + 1

매트릭스에서 대각선 왼쪽 위로 보이는 건 이 정의의 시각화일 뿐이었다. 정의로 생각하니 바로 납득됐다.

Case 2: A[i] != B[j]

두 문자가 다르면 둘 중 하나를 없는 사람 취급한다.
LCS니까 더 긴 쪽을 선택한다.

dp[i][j] = max(dp[i-1][j], dp[i][j-1])

추가로 든 의문: 왜 대각선(dp[i-1][j-1])은 max에 포함 안 해도 되나?

Case 2에서 max(dp[i-1][j], dp[i][j-1])를 쓰는데, 대각선인 dp[i-1][j-1]도 고려해야 하지 않냐는 의문이 생겼다.

결론은 필요 없다.

dp[i-1][j-1] <= dp[i-1][j] 이고 dp[i-1][j-1] <= dp[i][j-1] 가 항상 성립한다.
한 칸 더 고려할수록 LCS는 같거나 늘어날 수는 있어도 줄지 않기 때문에, 대각선은 max에서 어차피 선택되지 않는다.


구현

경계 처리를 위해 dp 테이블을 (n+1) x (m+1)로 잡고, 0번째 행/열을 0으로 초기화한다.
A와 B의 인덱스는 1-based dp 인덱스에서 -1 오프셋으로 접근한다.

#include <iostream>
#include <bits/stdc++.h>
using namespace std;

int main() {
    string A, B;
    cin >> A >> B;

    vector<vector<int>> dp(A.size() + 1, vector<int>(B.size() + 1));

    for (int i = 1; i <= A.size(); i++) {
        for (int j = 1; j <= B.size(); j++) {
            if (A[i-1] != B[j-1])
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            else
                dp[i][j] = dp[i-1][j-1] + 1;
        }
    }

    cout << dp[A.size()][B.size()];
    return 0;
}

답은 dp[A.size()][B.size()].


정리

외우려고 하면 헷갈린다. dp[i][j]의 정의를 정확히 붙잡고, 각 케이스에서 "무엇을 소비했는가"를 생각하면 점화식은 자연스럽게 따라온다.