LCS

최장 공통 부분수열 vs 최장 공통 문자열

LCS라 하면 주로 최장 공통 부분수열(Logest Common Subsequence)를 말하지만, 최장 공통 문자열(Logest Common Substring)을 말하기도 한다.
Attachments/Picture/Pasted image 20250206105159.png
최장 공통 부분수열 (Longest Common Subsequence, LCS)

최장 공통 문자열 (Longest Common Substring)

두 경우 모두 이전의 최댓값을 기반으로 찾기 때문에 Dynamic Programming 사용하여 해결할 수 있다.

Logest common Substring

점화식

A[i-1] == B[j-1]이면
	dp[i][j] = dp[i-1][j-1] + 1
	
A[i-1] != B[j-1]이면
	dp[i][j] = 0

공통 문자열은 연속되어야한다. 그래서 두 문자의 앞 글자까지의 공통 문자열이 있다면 계속 연속으로 이어지기 때문에 위와 점화식이 나온다.

과정

문자열 A, B 의 최장 공통 문자열을 구한다고 했을 때

  1. 두 문자열의 각 문자를 비교
  2. 문자가 같으면 dp[i][j] = dp[i-1][j-1] + 1 로 업데이트
  3. 문자가 다르면 dp[i][j] = 0으로 설정
  4. DP 테이블에서 최댓값을 찾고, 해당 위치에서 문자열을 추출

ABCDEFGBCDFE 의 공통 문자열을 구하는 경우 DP 배열이 아래와 같은 결과를 갖는다.
DP 배열의 최댓값이 최장 공통 문자열의 길이가 된다.
Attachments/Picture/Pasted image 20250206110505.png|400
출처

코드

def longest_common_substring(A, B):
    m, n = len(A), len(B)
    dp = [[0] * (n+1) for _ in range(m+1)]
    max_length = 0
    end_pos = 0

    for i in range(1, m+1):
        for j in range(1, n+1):
            if A[i-1] == B[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
                if dp[i][j] > max_length:
                    max_length = dp[i][j]
                    end_pos = i
            else:
                dp[i][j] = 0

    return A[end_pos - max_length:end_pos]

A = "ABCDF"
B = "BCDE"
print("Longest Common Substring:", longest_common_substring(A, B))

Longest Common Subsequence

점화식

A[i-1] == B[j-1]이면
    dp[i][j] = dp[i-1][j-1] + 1
A[i-1] != B[j-1]이면
    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

과정

  1. 두 문자열의 각 문자를 비교
  2. 문자가 같으면 dp[i][j] = dp[i-1][j-1] + 1
  3. 문자가 다르면 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  4. 마지막 dp 값이 최장 길이

ABCDEFGBCDFE 의 예시
Attachments/Picture/Pasted image 20250206112313.png|400

코드

def longest_common_subsequence(A, B):
    m, n = len(A), len(B)
    dp = [[0] * (n+1) for _ in range(m+1)]

    for i in range(1, m+1):
        for j in range(1, n+1):
            if A[i-1] == B[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])

    return dp[m][n]

A = "ABCDF"
B = "ACDF"
print("Longest Common Subsequence Length:", longest_common_subsequence(A, B))

역추적

최장 공통 부분수열은 공통 문자열과 다르게 부분수열을 찾는게 조금 복잡하다.
문자열이 달라도 이전 최댓값을 가져오기 때문에

  1. dp[i-1][j], dp[i][j-1] 중에 같은 값으로 이동
  2. 같은 값이 없으면 dp[i-1][j-1] 로 이동
  3. 이동할 수 없거나,

Attachments/Picture/Pasted image 20250206113307.png|400

코드

def lcs_backtracking(A, B):
    m, n = len(A), len(B)
    dp = [[0] * (n+1) for _ in range(m+1)]

    for i in range(1, m+1):
        for j in range(1, n+1):
            if A[i-1] == B[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])

    lcs = []
    i, j = m, n
    while i > 0 and j > 0:
        if A[i-1] == B[j-1]:
            lcs.append(A[i-1])
            i -= 1
            j -= 1
        elif dp[i-1][j] > dp[i][j-1]:
            i -= 1
        else:
            j -= 1

    return ''.join(reversed(lcs))

A = "ABCDF"
B = "ACDF"
print("Longest Common Subsequence:", lcs_backtracking(A, B))

역추적 조건을 백준 9252번 - LCS 2#정답 코드 에서 처럼 dp값이 0 인지 체크할 수도 있다.

관련 문제

Reference