티스토리 뷰
백준 9251번: LCS (최장 공통 부분 수열)
문제 설명
백준 9251번 LCS 문제는 두 문자열이 주어졌을 때, 두 문자열에 모두 포함된 가장 긴 공통 부분 수열의 길이를 구하는 문제입니다. 공통 부분 수열은 두 문자열에서 순서를 유지하며 공통으로 나타나는 문자들의 시퀀스를 말합니다. 예를 들어, "ACAYKP"와 "CAPCAK"의 공통 부분 수열에는 "CA", "ACA", "ACAK" 등이 있으며, 이 중 가장 긴 것은 "ACAK"로 길이가 4입니다. 이 문제는 두 문자열을 입력받아 최장 공통 부분 수열의 길이를 출력해야 합니다.
입력
- 첫 번째 줄: 첫 번째 문자열 (최대 1000자, 공백 없음).
- 두 번째 줄: 두 번째 문자열 (최대 1000자, 공백 없음).
출력
- 두 문자열의 최장 공통 부분 수열의 길이.
테스트 케이스
예제 입력:
ACAYKP
CAPCAK
예제 출력:
4
설명: "ACAYKP"와 "CAPCAK"의 최장 공통 부분 수열은 "ACAK"로, 길이는 4입니다.
추가 테스트 케이스:
- 입력: "CPA"와 "ACAY"
출력: 2 (공통 부분 수열: "CA") - 입력: "ABC"와 "DEF"
출력: 0 (공통 문자 없음) - 입력: "AAA"와 "AAA"
출력: 3 (공통 부분 수열: "AAA")
문제 작동 원리
LCS 문제는 두 문자열에서 순서를 유지하는 공통된 문자들의 최대 길이를 찾는 것입니다. 공통 부분 수열은 연속적일 필요는 없지만, 각 문자열에서의 문자 순서가 유지되어야 합니다.
답이 되는 경우:
- "ACAYKP"와 "CAPCAK"에서 "ACAK"는 유효한 공통 부분 수열입니다. "ACAYKP"에서 'A'(1), 'C'(2), 'A'(3), 'K'(5)를, "CAPCAK"에서 'C'(1), 'A'(2), 'C'(4), 'K'(6)를 선택하면 순서가 유지됩니다.
- "CPA"와 "ACAY"에서 "CA"는 유효합니다. "CPA"에서 'C'(1), 'A'(3), "ACAY"에서 'C'(2), 'A'(3).
답이 아닌 경우:
- "ACAYKP"에서 "AYK"는 "CAPCAK"에서 순서가 맞지 않아 공통 부분 수열이 될 수 없습니다("CAPCAK"에 'Y'가 없음).
- "CPA"와 "ACAY"에서 "CPA"는 "ACAY"에 'P'가 없으므로 불가능.
사례:
- "CPA"와 "ACAY":
- 공통 부분 수열: "C", "A", "CA" 가능.
- "CA"는 길이 2로 최장.
- "CPA"는 "ACAY"에 'P'가 없어 불가능.
- "ABC"와 "DEF":
- 공통 문자가 없으므로 길이 0.
문제는 두 문자열의 끝 문자를 비교하며 공통 부분을 찾고, 이를 체계적으로 확장해 최대 길이를 구하는 방식으로 해결됩니다.
아이디어
재귀적 접근: "끝자리를 보자!"
LCS는 두 문자열에서 순서를 유지하는 공통된 문자들의 최대 길이를 찾는 문제입니다. 직관적으로, 두 문자열의 끝 문자를 먼저 보면서 문제를 작게 나누는 방식으로 접근했습니다. 이 "끝자리를 보자!" 논리는 재귀의 핵심입니다. 끝 문자가 같거나 다를 때 다른 전략을 취하며, 이를 통해 공통 부분 수열을 찾아갑니다.
단계별 재귀: 문자열 쌍의 변화
"ACAYKP"와 "CAPCAK"를 예로 들어, 재귀적으로 문자열 쌍이 어떻게 줄어드는지 따라가 보겠습니다. 각 단계에서 끝 문자를 비교하고, 같을 때와 다를 때의 선택 원리를 명확히 합니다.
단계 1: "ACAYKP"와 "CAPCAK"
ACAYK[P]
CAPCA[K]
- 끝 문자: 'P' ≠ 'K' (다름).
- 선택 원리 (다를 때):
- 끝 문자가 다르면, 공통 부분 수열에 'P'나 'K'를 포함할 수 없습니다. 두 가지 경우를 고려:
- 첫 번째 문자열의 끝 문자('P')를 제외: "ACAYK"와 "CAPCAK"의 공통 부분 수열.
- 두 번째 문자열의 끝 문자('K')를 제외: "ACAYKP"와 "CAPCA"의 공통 부분 수열.
- 두 경우 중 더 긴 공통 부분 수열을 선택.
- 끝 문자가 다르면, 공통 부분 수열에 'P'나 'K'를 포함할 수 없습니다. 두 가지 경우를 고려:
- 다음 단계:
- "ACAYK"와 "CAPCAK" (첫 번째 문자열에서 'P' 제외).
- "ACAYKP"와 "CAPCA" (두 번째 문자열에서 'K' 제외).
- 느낌: 'P'와 'K'가 달라 둘 중 하나를 버리고, 나머지 문자열로 답을 찾아가는 퍼즐 조각 선택.
단계 2: "ACAYK"와 "CAPCAK"
ACAY[K]
CAPCA[K]
- 끝 문자: 'K' = 'K' (같음).
- 선택 원리 (같을 때):
- 끝 문자가 같으면, 'K'를 공통 부분 수열에 포함하고, 나머지 문자열("ACAY"와 "CAPCA")의 공통 부분 수열을 찾습니다.
- 길이 = "ACAY"와 "CAPCA"의 공통 부분 수열 길이 + 1('K').
- 다음 단계: "ACAY"와 "CAPCA".
- 느낌: 'K'가 같아 퍼즐 조각 하나를 맞췄다! 'K'를 추가하고, 나머지 조각으로 넘어감.
단계 3: "ACAY"와 "CAPCA"
ACA[Y]
CAPC[A]
- 끝 문자: 'Y' ≠ 'A' (다름).
- 선택 원리 (다를 때):
- 'Y'와 'A'는 다르므로, 두 가지 경우:
- "ACA"와 "CAPCA" (첫 번째 문자열에서 'Y' 제외).
- "ACAY"와 "CAPC" (두 번째 문자열에서 'A' 제외).
- 더 긴 공통 부분 수열 선택.
- 'Y'와 'A'는 다르므로, 두 가지 경우:
- 다음 단계:
- "ACA"와 "CAPCA".
- "ACAY"와 "CAPC".
- 느낌: 또 다른 선택지! 'Y'나 'A'를 버리고, 더 나은 길을 찾기 위해 두 경로 탐색.
단계 4: "ACA"와 "CAPCA"
AC[A]
CAPC[A]
- 끝 문자: 'A' = 'A' (같음).
- 선택 원리 (같을 때):
- 'A'를 공통 부분 수열에 포함.
- 길이 = "AC"와 "CAPC"의 공통 부분 수열 길이 + 1('A').
- 다음 단계: "AC"와 "CAPC".
- 느낌: 'A'가 맞아 또 한 조각 추가! 퍼즐이 점점 완성됨.
단계 5: "AC"와 "CAPC"
[A]C
CAP[C]
- 끝 문자: 'C' = 'C' (같음).
- 선택 원리 (같을 때):
- 'C'를 포함.
- 길이 = "A"와 "CAP"의 공통 부분 수열 길이 + 1('C').
- 다음 단계: "A"와 "CAP".
- 느낌: 'C'가 또 맞음! 계속 조각을 맞춰가는 중.
단계 6: "A"와 "CAP"
[A]
CA[P]
- 끝 문자: 'A' ≠ 'P' (다름).
- 선택 원리 (다를 때):
- 두 가지 경우:
- 빈 문자열과 "CAP" (첫 번째 문자열에서 'A' 제외).
- "A"와 "CA" (두 번째 문자열에서 'P' 제외).
- 더 긴 공통 부분 수열 선택.
- 두 가지 경우:
- 다음 단계:
- 빈 문자열과 "CAP" (길이 0).
- "A"와 "CA".
- 느낌: 'A'와 'P'가 달라, 두 경로 중 하나 선택.
단계 7: "A"와 "CA"
[A]
C[A]
- 끝 문자: 'A' = 'A' (같음).
- 선택 원리 (같을 때):
- 'A'를 포함.
- 길이 = 빈 문자열과 "C"의 공통 부분 수열(0) + 1('A') = 1.
- 다음 단계: 빈 문자열과 "C" (길이 0).
- 느낌: 'A'가 맞아 최종적으로 길이 1 추가.
재귀 결과:
위 과정을 역으로 추적:
- "A"와 "CA" → 길이 1 ("A").
- "AC"와 "CAPC" → "A"와 "CAP" + 1('C') = 1 + 1 = 2 ("AC").
- "ACA"와 "CAPCA" → "AC"와 "CAPC" + 1('A') = 2 + 1 = 3 ("ACA").
- "ACAYK"와 "CAPCAK" → "ACAY"와 "CAPCA" + 1('K') = 3 + 1 = 4 ("ACAK").
- "ACAYKP"와 "CAPCAK" → "ACAYK"와 "CAPCAK" = 4 (최종 "ACAK").
작은 문자열 쌍 예시:
- "A"와 "C":
[A] [C]
- 'A' ≠ 'C' → 빈 문자열과 "C"(0), "A"와 빈 문자열(0) → 길이 0.
- "AC"와 "CA":
A[C] C[A]
- 'C' ≠ 'A' → "A"와 "CA"(1), "AC"와 "C"(1) → 길이 1.
- "ACA"와 "CA":
AC[A] C[A]
- 'A' = 'A' → "AC"와 "C"(1) + 1 = 2 ("CA").
- "CPA"와 "ACAY":
CP[A] ACA[Y]
- 'A' ≠ 'Y' → "CP"와 "ACAY"(1), "CPA"와 "ACA"(2) → 길이 2 ("CA").
동적계획법으로 변환
재귀적 접근은 직관적이지만, 같은 문자열 쌍을 여러 번 계산하는 비효율성이 있습니다. 이를 해결하기 위해 동적 프로그래밍(DP)을 사용합니다. 재귀에서는 큰 문자열 쌍에서 작은 쌍으로 줄여가며 탐색했다면, DP에서는 반대로 작은 쌍부터 시작해 큰 쌍으로 확장합니다. 이는 재귀의 논리를 역으로 적용해 표를 채우는 방식으로, 각 문자열 쌍의 결과를 한 번만 계산해 저장합니다.
DP 표 채우기 예시: "CPA"와 "ACAY"
아래는 "CPA"와 "ACAY"를 예로 들어 DP 표를 채우는 과정을 8개 칸에 대해 설명한 것입니다. 각 칸은 재귀적 논리를 역으로 적용해 계산됩니다:
- 문자가 같을 때: 끝 문자를 포함하고, 나머지 문자열 쌍의 길이에 1을 더함 (대각선 위 칸 사용).
- 문자가 다를 때: 두 가지 경우(첫 번째 문자열 끝 제외, 두 번째 문자열 끝 제외) 중 더 큰 값 선택 (왼쪽 또는 위쪽 칸 사용).
초기 표 (빈 문자열 포함, 첫 행과 열은 0으로 초기화):
1. "C"와 "A"
- 비교 문자: 'C'와 'A' (다름).
- 원리: 문자가 다르므로, 두 가지 경우:
- 빈 문자열과 "A": 표의 첫 번째 행, "A" 열 → 0.
- "C"와 빈 문자열: 표의 "C" 행, 첫 번째 열 → 0.
- 더 큰 값: $\max(0, 0) = 0$.
- 결과: 길이 0.
표 업데이트:
2. "C"와 "AC"
- 비교 문자: 'C'와 'C' (같음).
- 원리: 문자가 같으므로, 'C'를 포함하고 빈 문자열과 "A"의 길이에 1을 더함:
- 빈 문자열과 "A": 표의 첫 번째 행, "A" 열 → 0.
- 따라서: $0 + 1 = 1$.
- 결과: 길이 1 ("C").
표 업데이트:
3. "C"와 "ACA"
- 비교 문자: 'C'와 'A' (다름).
- 원리: 문자가 다르므로:
- 빈 문자열과 "ACA": 표의 첫 번째 행, "ACA" 열 → 0.
- "C"와 "AC": 표의 "C" 행, "AC" 열 → 1.
- 더 큰 값: $\max(0, 1) = 1$.
- 결과: 길이 1.
표 업데이트:
4. "C"와 "ACAY"
- 비교 문자: 'C'와 'Y' (다름).
- 원리: 문자가 다르므로:
- 빈 문자열과 "ACAY": 표의 첫 번째 행, "ACAY" 열 → 0.
- "C"와 "ACA": 표의 "C" 행, "ACA" 열 → 1.
- 더 큰 값: $\max(0, 1) = 1$.
- 결과: 길이 1.
표 업데이트:
5. "CP"와 "A"
- 비교 문자: 'P'와 'A' (다름).
- 원리: 문자가 다르므로:
- "C"와 "A": 표의 "C" 행, "A" 열 → 0.
- "CP"와 빈 문자열: 표의 "CP" 행, 첫 번째 열 → 0.
- 더 큰 값: $\max(0, 0) = 0$.
- 결과: 길이 0.
표 업데이트:
6. "CP"와 "AC"
- 비교 문자: 'P'와 'C' (다름).
- 원리: 문자가 다르므로:
- "C"와 "AC": 표의 "C" 행, "AC" 열 → 1.
- "CP"와 "A": 표의 "CP" 행, "A" 열 → 0.
- 더 큰 값: $\max(1, 0) = 1$.
- 결과: 길이 1.
표 업데이트:
7. "CP"와 "ACA"
- 비교 문자: 'P'와 'A' (다름).
- 원리: 문자가 다르므로:
- "C"와 "ACA": 표의 "C" 행, "ACA" 열 → 1.
- "CP"와 "AC": 표의 "CP" 행, "AC" 열 → 1.
- 더 큰 값: $\max(1, 1) = 1$.
- 결과: 길이 1.
표 업데이트:
8. "CP"와 "ACAY"
- 비교 문자: 'P'와 'Y' (다름).
- 원리: 문자가 다르므로:
- "C"와 "ACAY": 표의 "C" 행, "ACAY" 열 → 1.
- "CP"와 "ACA": 표의 "CP" 행, "ACA" 열 → 1.
- 더 큰 값: $\max(1, 1) = 1$.
- 결과: 길이 1.
표 업데이트:
느낌: 표는 작은 접두사 쌍부터 채워지며, 각 칸은 재귀적 논리를 역으로 적용합니다. 예를 들어, "C"와 "AC"에서 'C'가 같아 길이 1로 증가하고, "CP"와 "ACAY"에서는 'P'가 없어 이전 결과(1)를 유지합니다.
전체 코드
arr1=input()
arr2=input()
N1=len(arr1)
N2=len(arr2)
dp=[[0 for _ in range(1001)] for _ in range(1001)]
for i in range(1,N1+1):
for j in range(1,N2+1):
if arr1[i-1]==arr2[j-1]:
dp[i][j]=dp[i-1][j-1]+1
else:
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
print(dp[N1][N2])
개별 코드 설명
코드를 단계별로 설명하며, 표 채우기 과정과 문자열 쌍의 변화를 보여드립니다. "CPA"와 "ACAY"를 예로 들어, i-1, j-1의 필요성과 dp[i-1][j], dp[i][j-1]에 어떤 문자열 쌍이 들어가는지 설명합니다.
입력 처리
arr1=input()
arr2=input()
N1=len(arr1)
N2=len(arr2)
- 작동: arr1에 "CPA", arr2에 "ACAY"를 입력받아, N1=3, N2=4. 파이썬 문자열은 0-based이므로 arr1[0]='C', arr1[1]='P', arr1[2]='A', arr2[0]='A', arr2[1]='C', ...
- 문자열 쌍: "CPA"의 부분 문자열("C", "CP", "CPA"), "ACAY"의 부분 문자열("A", "AC", "ACA", "ACAY")이 표의 행과 열로 매핑.
- 느낌: 두 문자열을 준비해 표의 행과 열을 정의하는 단계입니다.
초기화
dp=[[0 for _ in range(1001)] for _ in range(1001)]
- 작동: dp 배열의 첫 행과 열은 0으로 초기화. 이는 빈 문자열($\epsilon$)과 다른 문자열의 공통 부분 수열 길이가 0임을 의미.
- 문자열 쌍:
- dp[0][1]: 빈 문자열과 "A" → 0.
- dp[1][0]: "C"와 빈 문자열 → 0.
- dp[2][0]: "CP"와 빈 문자열 → 0.
- 표:
- 느낌: 퍼즐의 테두리를 그리는 단계로, 빈 문자열 쌍을 처리.
표 채우기
for i in range(1,N1+1):
for j in range(1,N2+1):
if arr1[i-1]==arr2[j-1]:
dp[i][j]=dp[i-1][j-1]+1
else:
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
- 작동: 행 단위로 표를 채웁니다. 각 dp[i][j]는 첫 번째 문자열의 처음 i개 문자와 두 번째 문자열의 처음 j개 문자의 공통 부분 수열 길이.
- 왜 i-1, j-1인가?: arr1과 arr2는 0-based 문자열(arr1[0]은 첫 문자). 하지만 dp[i][j]는 i개, j개 문자를 의미하므로, i=1은 첫 번째 문자를, j=1은 첫 번째 문자를 가리킴. 따라서 arr1[i-1]과 arr2[j-1]로 현재 문자를 비교.
- 예: i=1, j=1에서 arr1[0]='C', arr2[0]='A'를 비교해 "C"와 "A"를 처리.
첫 번째 행: "C" (i=1)
- "C"와 "A" (j=1):
- arr1[0]='C', arr2[0]='A'. 다름.
- dp[1][1] = $\max(dp[0][1], dp[1][0]) = \max(0, 0) = 0$.
- 문자열 쌍: dp[0][1]는 빈 문자열과 "A"(0), dp[1][0]은 "C"와 빈 문자열(0).
- "C"와 "AC" (j=2):
- arr1[0]='C', arr2[1]='C'. 같음.
- dp[1][2] = dp[0][1] + 1 = 0 + 1 = 1.
- 문자열 쌍: dp[0][1]는 빈 문자열과 "A"(0). 'C'를 추가해 길이 1.
- "C"와 "ACA" (j=3):
- arr1[0]='C', arr2[2]='A'. 다름.
- dp[1][3] = $\max(dp[0][3], dp[1][2]) = \max(0, 1) = 1$.
- 문자열 쌍: dp[0][3]는 빈 문자열과 "ACA"(0), dp[1][2]은 "C"와 "AC"(1).
- "C"와 "ACAY" (j=4):
- arr1[0]='C', arr2[3]='Y'. 다름.
- dp[1][4] = $\max(dp[0][4], dp[1][3]) = \max(0, 1) = 1$.
- 문자열 쌍: dp[0][4]는 빈 문자열과 "ACAY"(0), dp[1][3]은 "C"와 "ACA"(1).
표:
느낌: 첫 번째 행은 "C"와 모든 두 번째 문자열의 부분 문자열을 처리. "C"와 "AC"에서 공통 문자 'C'를 찾아 길이 1로 업데이트.
두 번째 행: "CP" (i=2)
- "CP"와 "A" (j=1):
- arr1[1]='P', arr2[0]='A'. 다름.
- dp[2][1] = $\max(dp[1][1], dp[2][0]) = \max(0, 0) = 0$.
- 문자열 쌍: dp[1][1]은 "C"와 "A"(0), dp[2][0]은 "CP"와 빈 문자열(0).
- "CP"와 "AC" (j=2):
- arr1[1]='P', arr2[1]='C'. 다름.
- dp[2][2] = $\max(dp[1][2], dp[2][1]) = \max(1, 0) = 1$.
- 문자열 쌍: dp[1][2]은 "C"와 "AC"(1), dp[2][1]은 "CP"와 "A"(0).
- "CP"와 "ACA" (j=3):
- arr1[1]='P', arr2[2]='A'. 다름.
- dp[2][3] = $\max(dp[1][3], dp[2][2]) = \max(1, 1) = 1$.
- 문자열 쌍: dp[1][3]은 "C"와 "ACA"(1), dp[2][2]은 "CP"와 "AC"(1).
- "CP"와 "ACAY" (j=4):
- arr1[1]='P', arr2[3]='Y'. 다름.
- dp[2][4] = $\max(dp[1][4], dp[2][3]) = \max(1, 1) = 1$.
- 문자열 쌍: dp[1][4]은 "C"와 "ACAY"(1), dp[2][3]은 "CP"와 "ACA"(1).
표:
느낌: 두 번째 행은 "CP"와 모든 열을 처리. 'P'는 "ACAY"에 없으므로 길이가 늘어나지 않음.
max의 논리와 업데이트
- 왜 max?:
- 끝 문자가 다를 때(예: "CP"와 "ACAY"의 'P'와 'Y'), 두 가지 경우를 고려:
- 첫 번째 문자열에서 끝 문자 제외: "C"와 "ACAY"($dp[i-1][j]$).
- 두 번째 문자열에서 끝 문자 제외: "CP"와 "ACA"($dp[i][j-1]$).
- 더 긴 공통 부분 수열을 선택해 현재 문자열 쌍의 길이를 유지.
- 끝 문자가 다를 때(예: "CP"와 "ACAY"의 'P'와 'Y'), 두 가지 경우를 고려:
- 업데이트되는 글자:
- 끝 문자 같음: 새 문자가 공통 부분 수열에 추가(예: "C"와 "AC"에서 'C' 추가로 길이 1).
- 끝 문자 다름: 새 문자는 추가되지 않고, 이전 문자열 쌍의 공통 문자들(예: "C")로 길이 유지.
출력
print(dp[N1][N2])
- 작동: dp[3][4]는 "CPA"와 "ACAY"의 LCS 길이 2("CA")를 출력.
결론
- 재귀에서 DP로의 전환: 재귀는 중복 계산으로 비효율적이지만, DP는 표를 사용해 각 문자열 쌍을 한 번만 계산해 효율적입니다.
- 행 단위 표 채우기: 코드의 루프는 행 단위로 표를 채우며, "C", "CP", "CPA" 같은 문자열 쌍이 각 행에 매핑됩니다.
- 인덱스 조정의 중요성: i-1, j-1은 0-based 문자열과 1-based DP 테이블을 맞추기 위해 필요하며, 올바른 문자를 비교합니다.
- max의 역할: 끝 문자가 다를 때, 두 가지 경우("C"와 "ACAY", "CP"와 "ACA") 중 더 긴 공통 부분 수열을 선택해 최적의 길이를 유지합니다.
'백준 스터디' 카테고리의 다른 글
백준 2012번: 등수 매기기 문제 해설 (Python) (4) | 2025.08.06 |
---|---|
백준 2012번: 등수 매기기 문제 해설 (C++) (1) | 2025.08.06 |
백준 9251번: LCS (최장 공통 부분 수열) (1) | 2025.08.05 |
백준 2217 로프 문제 Python (1) | 2025.08.04 |
백준 2217 로프 문제 C++ (0) | 2025.08.04 |
- Total
- Today
- Yesterday
- 프로그래밍
- DP
- 알고리즘 문제풀이
- c언어
- 코딩 테스트
- 알고리즘문제풀이
- 문제 풀이
- 문자열처리
- 그래프 탐색
- 문제풀이
- 브루트포스
- 코딩
- 객체지향
- 파이썬
- python 알고리즘
- 동적 계획법
- 코딩테스트
- 동적계획법
- C++
- 그리디
- 알고리즘기초
- C++ 알고리즘
- Python
- dfs
- 그리디알고리즘
- 백준
- 파이썬코딩
- 알고리즘
- c++알고리즘
- 인접 행렬
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |