Computer Science
LCS(최장 공통 부분 수열 & 문자열 DP 문제)
Balang
2025. 4. 22. 09:05
728x90
문제 개요: “두 문자열에서 가장 긴 공통 부분 수열을 찾아라”
LCS(Longest Common Subsequence) 문제는,
두 문자열 A와 B가 주어졌을 때
"순서를 유지하면서 가장 긴 공통 부분 수열"의 길이를 구하는 문제입니다.
주의: 부분 문자열(substring)이 아니라 부분 수열(subsequence)입니다.
예시:
A = "ACAYKP"
B = "CAPCAK"
→ LCS = "ACAK" → 길이 = 4
부분 수열 vs 부분 문자열 차이
항목부분 | 수열부분 | 문자열 |
정의 | 순서 유지, 중간 문자 건너뛰기 가능 | 연속된 문자의 부분 |
예 | "ACAYKP" → "AYK", "AK" 가능 | "ACAYKP" → "ACA"만 가능 |
문제 예시 | LCS, 부분 집합 문제 | 슬라이딩 윈도우, 연속 최대합 등 |
핵심 사고 방식: DP 테이블로 두 문자열 비교
테이블 정의:
- dp[i][j]: A 문자열의 앞 i글자와 B 문자열의 앞 j글자에서의 LCS 길이
점화식 유도:
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])
즉, 문자가 같다면 공통 수열에 포함
다르면 지금까지의 최장 수열 중 더 긴 쪽을 선택
시각적으로 점화식 이해하기
A: A C A Y K P
B: C A P C A K
↓ dp 테이블 초기화 예시
0 0 0 0 0 0 0
0
0
0
0
0
0
- 두 문자열의 길이 + 1 만큼 2차원 배열 초기화
- 0행/0열은 "빈 문자열"과의 비교
Python 코드
def lcs(a, b):
n, m = len(a), len(b)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 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[n][m]
만일 최장 공통 수열 자체를 출려하려면
단순히 길이만 구하는 게 아니라
LCS 문자열 자체를 복원하려면 역추적이 필요합니다.
def get_lcs(a, b):
n, m = len(a), len(b)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 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])
# 역추적
i, j = n, m
result = []
while i > 0 and j > 0:
if a[i - 1] == b[j - 1]:
result.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(result))
시간/공간 복잡도
항목 | 복잡도 |
시간 | O(n × m) |
공간 | O(n × m) (길이만 구한다면 O(min(n, m))도 가능) |
- 문자열 길이가 작으면 그대로 쓰고,
메모리 제한이 있을 경우 1D 배열 최적화도 가능
요약
- LCS는 순서를 유지하는 공통 수열 중 가장 긴 것을 찾는 문제
- 핵심은 2차원 DP 테이블 설계와
“문자가 같으면 대각선 +1, 다르면 위/왼쪽 중 큰 값” 점화식 - 복원하려면 **역추적(backtracking)**을 추가해야 함
- Git, Bio, 에디터, 추천 시스템 등 실무 활용도 높음
728x90
반응형