LCS의 정의
- 가장 긴 공통 부분 문자열 (Longest Common Substring)
- 가장 긴 공통 부분 수열 (Longest Common Sequence)
- N^2 풀이
- 경로 추적
- N*logN 풀이 - Using LIS
- 문자열이 N개일 때
- 메모리 최적화
두 문자열의 가장 긴 부분 문자열이다.
문자열이란 끊어지지 않는 문자들의 연속이다.
두 문자열의 가장 긴 부분 수열이다.
부분 수열이란 원래 수열에서 임의의 원소를 0개 이상 지워서 만든 수열이다.
이 포스팅에서는 가장 긴 공통 부분 수열을 다루며,
다루어지는 주제는 다음과 같다.
N^2 풀이
동적 계획법을 사용하여 문제를 풀어보자.
작은 문제부터 시작하여 큰 문제를 해결하는 것 이다!
int solve (int i, int j) // A[0...i]와 B[0...j]에서, 최대 부분 수열의 길이
위와 같이 정의하면, 다음과 같은 점화식이 유도되며,
LCS(i, j)의 값을 Cache[i][j]에 캐싱하면 반복문으로도 해결할 수 있다.
재귀를 추적하면 A[0], B[0]에서 시작해서.
매 단계마다 A 또는 B를 한칸씩 증가시키면서,
모든 A[i], B[j]의 쌍에 대해 위의 루틴을 실행한다.
A[i]와 B[j]가 동일한 경우에는,
부분 수열의 길이가 1 늘어난 것이고,
A[i], B[j]가 동일하지 않은 경우에는,
지금까지의 부분 수열의 최대 길이를 반환해야 한다.
TOP-DOWN
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 100;
int cache[SIZE+1][SIZE+1];
char A[SIZE+1];
char B[SIZE+1];
int LCS(int i, int j){
//! base condition.
if(i==0 || j==0) return 0;
//! memoization.
if(cache[i][j] != -1) return cache[i][j];
//! solving.
if(A[i] == B[j]) cache[i][j] = LCS(i-1, j-1) + 1;
else cache[i][j] = max(LCS(i-1, j), LCS(i, j-1));
return cache[i][j];
}
int main()
{
cin.tie(nullptr);
ios::sync_with_stdio(false);
memset(cache, -1, sizeof cache);
cin >> (A+1);
cin >> (B+1);
cout << LCS(strlen(A+1), strlen(B+1));
}
BOTTOM-UP
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 100;
int cache[SIZE+1][SIZE+1];
char A[SIZE+1];
char B[SIZE+1];
int main()
{
cin.tie(nullptr);
ios::sync_with_stdio(false);
memset(cache, 0, sizeof cache);
cin >> (A+1);
cin >> (B+1);
for(int i=1; i<=strlen(A+1); i++){
for(int j=1; j<=strlen(B+1); j++){
if(A[i]==B[j]) cache[i][j] = cache[i-1][j-1]+1;
else cache[i][j] = max(cache[i-1][j], cache[i][j-1]);
}
}
cout << cache[strlen(A+1)][strlen(B+1)];
}경로 추적
다음은 cache[i][j]를 시각화 한 것 이다.
cache[i][j]가 갱신되는 케이스는 아래와 같다.
- if A[ i ]==B[ j ] ) LCS(i, j) = LCS(i-1, j-1) + 1
- if A[ i ] !=B[ j ] ) LCS(i, j) = max(LCS(i-1, j), LCS(i, j-1))
좌측 또는 상단에 자신과 같은 값이 있다면,
A[i]와 B[j]는 매칭되지 않아, 이전 최댓 값을 유지하려고 하는 경우이므로.
좌측 또는 상단의 최댓값 방향으로 이동하면 된다.
만약 그렇지 않다면, A[i]와 B[j]가 매칭되어 +1된 경우이므로.
해당 문자를 스택에 삽입하고, 왼쪽 위 대각선 방향으로 이동하면 된다.
문자열이 N개 일 때 LCS
2개인 경우에서 재귀를 추적했을 때.
A[0], B[0]에서 시작하여, 하나를 선택해서 증가시키고 LCS를 계산했다.
일반화 하면 N개의 단어 중, 하나를 선택해서 증가시키고 LCS를 계산하면 된다.
TOP-DOWN
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 100;
int cache[SIZE+1][SIZE+1][SIZE+1];
char A[SIZE+1];
char B[SIZE+1];
char C[SIZE+1];
int LCS(int i, int j, int k){
//! case condition.
if(i==0 || j==0 || k==0) return 0;
//! memoization.
if(cache[i][j][k] != -1) return cache[i][j][k];
//! solving.
if(A[i]==B[j] && B[j]==C[k]) {
cache[i][j][k] = LCS(i-1, j-1, k-1)+1;
}
else {
int val = 0;
val = max(val, LCS(i-1, j, k));
val = max(val, LCS(i, j-1, k));
val = max(val, LCS(i, j, k-1));
cache[i][j][k] = val;
}
return cache[i][j][k];
}
int main()
{
cin.tie(nullptr);
ios::sync_with_stdio(false);
memset(cache, -1, sizeof cache);
cin >> (A+1);
cin >> (B+1);
cin >> (C+1);
cout << LCS(strlen(A+1), strlen(B+1), strlen(C+1));
}
BOTTOM-UP
#include <bits/stdc++.h>
using namespace std;
const int SIZE = 100;
int cache[SIZE+1][SIZE+1][SIZE+1];
char A[SIZE+1];
char B[SIZE+1];
char C[SIZE+1];
int main()
{
cin.tie(nullptr);
ios::sync_with_stdio(false);
memset(cache, 0, sizeof cache);
cin >> (A+1);
cin >> (B+1);
cin >> (C+1);
for(int i=1; i<=strlen(A+1); i++){
for(int j=1; j<=strlen(B+1); j++){
for(int k=1; k<=strlen(C+1); k++){
if(A[i]==B[j] && B[j]==C[k]) {
cache[i][j][k] = cache[i-1][j-1][k-1]+1;
}
else {
int val = 0;
val = max(val, cache[i-1][j][k]);
val = max(val, cache[i][j-1][k]);
val = max(val, cache[i][j][k-1]);
cache[i][j][k] = val;
}
}
}
}
cout << cache[strlen(A+1)][strlen(B+1)][strlen(C+1)];
}
관련 문제
LCS - 1 페이지
www.acmicpc.net
'# Foundation > 알고리즘' 카테고리의 다른 글
| 비트셋으로 집합 표현하기 (Bitset) (0) | 2019.10.20 |
|---|---|
| 그래프 이론 (1) (0) | 2019.07.23 |
| 가장 긴 공통 부분 문자열 (LCS - Longest Common Substring) 알고리즘 (0) | 2019.07.01 |
| 가장 긴 증가하는 부분 수열 (LIS - Longest Increasing Subsequence) 알고리즘 (0) | 2019.06.30 |
| 동적 계획법(Dynamic Programming) 알고리즘 (0) | 2019.06.25 |