2023.07.17

LCS

Longest Common Subsequence 또는 최장 공통 부분 수열, 문자열

최장 공통 부분 수열은 주어진 수열 모두의 부분 수열이 되는 수열 중 가장 긴 수열을 찾는 문제이다.


A = {1,2,3,4,5,6}

B = {1,3,5,4,3,6}

두 수열에 관한 최장 공통 부분 수열은 {1,3,4,6}이며 길이는 4이다.

주의! 두 수열은 다른 길이를 가질 수 있다.


문제를 푸는 핵심은 각 수열을 차례대로 검사하면서 같은 문자일 때 이를 공통 부분 수열로 넣고, 이전 공통 부분 수열들과 비교해서 나은 것으로 선택하는 것이다.

이는 다음과 같은 점화식을 가진다.

if (i == 0 || j == 0) {
    LCS[i][j] = 0;
} else {
    if (A.charAt(i - 1) == B.charAt(j - 1)) {
        LCS[i][j] = LCS[i - 1][j - 1] + 1;
    } else {
        LCS[i][j] = Math.max(LCS[i - 1][j], LCS[i][j - 1]);
    }
}


LCS[i][j] 가 0인 경우는 수열이 빈 String인 경우, 즉 초기 값

LCS[i][j]가 LCS[i-1][j-1]+1인 경우, A와 B에 같은 문자가 있어서 수열의 개수가 증가.

LCS[i][j]가 Max(LCS[i-1][j], LCS[i][j-1])인 경우, 현재 수열과 이전 수열들을 비교해 나은 것으로 선택.


이렇게 A 수열과 B 수열을 차례대로 비교하면서 공통 부분 수열을 선택하면 다음과 같이 결과가 나오게 된다.

  0 A B C A H
0 0 0 0 0 0 0
G 0 0 0 0 0 0
B 0 0 1 1 1 1
H 0 0 1 1 1 2
C 0 0 1 2 2 2
A 0 1 1 2 3 3

결과적으로 마지막 LCS[A.length][B.length]에 가장 긴 배열의 개수가 저장된다.

String A = br.readLine();
String B = br.readLine();

int[][] LCS = new int[A.length() + 1][B.length() + 1]; // 수열 개수 저장

for (int i = 0; i <= A.length(); i++) {
    for (int j = 0; j <= B.length(); j++) {
        if (i == 0 || j == 0) {
            LCS[i][j] = 0;
        } else {
            if (A.charAt(i - 1) == B.charAt(j - 1)) {
                LCS[i][j] = LCS[i - 1][j - 1] + 1;
            } else {
                LCS[i][j] = Math.max(LCS[i - 1][j], LCS[i][j - 1]);
            }
        }
    }
}



최장 공통 수열 찾아가기

두 수열의 공통 개수를 위와 같이 찾을 수 있다. 하지만, 간혹 공통 수열의 원소를 찾아야 하는 경우가 있다. 처음에는 서로 값이 같을 때 업데이트를 통해서 가능한 경우의 수를 찾는 방식으로 생각을 했는데, 더 쉬운 방식이 있었다. 위에서 본 테이블을 통해서 보면 된다.

  0 A B C A H
0 0 0 0 0 0 0
G 0 0 0 0 0 0
B 0 0 1 1 1 1
H 0 0 1 1 1 2
C 0 0 1 2 2 2
A 0 1 1 2 3 3

마지막에 위치한 테이블은 어디에서 파생되었을까? 바로 옆인 A,A 위치이다. LCS를 업데이트를 할때 왼쪽과 위에서 최대값을 찾거나, 대각선 방향에서 +1 을 하는 방식으로 진행된다. 최장 공통 수열에 추가되는 조건은 서로 값이 같을 때 즉, 대각선 방향에서 +1 하는 방식이다.


그러면 다음과 같이 생각할 수 있다.

현재 값과 왼쪽,  값을 비교한다.

= 왼쪽,  값이 현재 값과 같다. -> 왼쪽,  한쪽 방향으로 탐색
= 왼쪽,  값이 현재 값보다 1 정도 작다 -> 대각선으로 이동 (현재 위치가 추가된 수열 -> 업데이트)
= 왼쪽,  값이 같지 않고, 한쪽이 크거나 현재값과 같다. (해당 방향으로 이동)

이렇게 찾으면 LCS의 역순이기 때문에 실제로 이를 다시 reverse 해서 출력하면 된다.



이 방식을 코드로 나타내면… DP 방식이라는 걸 알 수 있을 것이다.


public static StringBuilder sb = new StringBuilder();
public static String A, B;

public static void FindLCS(int[][] lcs, int x, int y) {
    if (x == 0 || y == 0) {
        return;
    }
    if (lcs[x - 1][y] == lcs[x][y - 1]) {
        if (lcs[x][y] == lcs[x - 1][y]) {
            FindLCS(lcs, x - 1, y);
        } else if (lcs[x][y] - 1 == lcs[x - 1][y]) {
            sb.append(A.charAt(x - 1));
            FindLCS(lcs, x - 1, y - 1);
        }
    } else {
        if (lcs[x - 1][y] > lcs[x][y - 1]) {
            FindLCS(lcs, x - 1, y);
        } else {
            FindLCS(lcs, x, y - 1);
        }
    }
}

...
이후 System.out.println(sb.reverse());  출력했다.

추천문제 9251번: LCS

Leave a comment