어느 한 문자열 X를 ACDAB라고 한다면, 부분 수열은 {A}, {C}, {D}, {A}, {B}, {A, C}, {A, C, D} … {A, C, D, A, B}가 된다.
LCS는 두 문자열을 비교할 때, 공통 수열 중 길이가 가장 긴 부분 수열을 의미한다.
기본적으로 DP는 작은 문제의 결과값을 통해서 결국 큰 문제를 해결하는 방식이다.
LCS는 끝 문자 비교를 통해서 더 작은 문자열을 비교하는 케이스로 문제를 작게 만든다.
끝 문자를 비교할 때는 두가지 경우의 수가 발생한다.
1. 끝 문자가 같은 경우
X : BDCAB
Y : ABCBDB
지금 상태에서 X의 마지막 문자와, Y의 마지막 문자가 일치한다.
문자열 X, Y의 LCS를 비교한다고 할 때 다음과 같이 생각해볼 수 있다.
마지막 문자를 뺀 X, Y 문자열 X’, Y’의 LCS길이와 기존의 X, Y의 LCS길이는 아래와 같이 표현할 수 있다.
이미 같은 문자열 B를 상수 1로 고려하고, B를 뺀 나머지 문자열에 대한 LCS를 구하여 더하면 기존 문자열의 LCS라고 볼 수 있다.
2. 끝 문자열이 다른 경우
X : BDCABA
Y : ABCBDB
두 문자열이 다른 경우에는 1번처럼 두 문자열에서 한 번에 문자를 뺄 수 없다. 그래서 X에서 맨 끝 문자열을 뺀 X’와 Y를 비교한 LCS와 Y’와 X의 LCS를 비교하여 더 큰 경우를 선택하게 된다. 더 큰 경우를 선택하는 이유는 우리가 구하고자 하는 바가 ‘최장 길이의 수열’이기 때문이다. 식으로 표현하면 아래와 같다.
3. 문자열 길이가 없는 경우
이 이상 문자 비교가 어렵기 때문에 0을 Return한다.
따라서 위 세가지를 종합하면 아래와 같은 LCS 점화식이 나온다.
LCS를 실제 구할때는 행렬, 2차원 배열을 사용해서 구할 수 있다고 한다.
다음 문자열을 통해서 실제 LCS의 길이를 구해보자.
X = ABCBDAB
Y = BDCABA
위 테이블을 활용해서 LCS 문자열이 어떤 문자들로 이루어져 있는지 알 수 있다. 이때 앞서 테이블을 채우기 위해 숫자를 거꾸로 따라가기 때문에 백트래킹 기법이 사용된다고 볼 수 있다.
Link: https://www.acmicpc.net/problem/9251
# Bottom-Up
import sys
input = sys.stdin.readline
sys.setrecursionlimit(10**5)
X, Y = [ input()[:-1] for _ in range(2) ]
dp = [ [ 0 ] * ( len(Y) + 1 ) for _ in range( len(X) + 1 ) ]
def LCS(i, j):
if not dp[i][j]:
if i == 0 or j == 0:
return 0
elif X[i - 1] != Y[j - 1]:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
else: # X[i] = Y[j]
dp[i][j] = dp[i-1][j-1]+ 1
return dp[i][j]
for i in range(1, len(X) + 1):
for j in range(1, len(Y) + 1):
LCS(i, j)
print(dp[len(X)][len(Y)])
# Top-Down
import sys
input = sys.stdin.readline
sys.setrecursionlimit(10**5)
X, Y = [ input()[:-1] for _ in range(2) ]
dp = [ [ 0 ] * ( len(Y) + 1 ) for _ in range( len(X) + 1 ) ]
def LCS(i, j):
if not dp[i][j]:
if i == 0 or j == 0:
return 0
elif X[i - 1] != Y[j - 1]:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
else: # X[i] = Y[j]
dp[i][j] = LCS(i - 1, j - 1) + 1
return dp[i][j]
LCS(len(X), len(Y))
print(dp[len(X)][len(Y)])
최적해를 구하는데 사용되는 방법 중 하나로, 그 순간 가장 최적이라고 생각하는 방법을 선택해나가는 방식이다.
그리디 알고리즘은 순간 지역적으로 최적의 값을 만들지만 그 값이 누적되었다 해도 그 결과가 최적의 결과라고 보장할 수는 없다.
하지만 그리디 알고리즘을 적용할 수 있는 문제라면 지역적으로 최적의 값들이 전역적으로 최적의 값이 된다.
동적 프로그래밍의 경우 전체 해답이 부분 해에 의존되어있는 상태이다. 그래서 일반적으로 상향식 접근을 통해 작은 문제를 먼저 해결한 후 그 답을 활용하여 전체문제를 해결한다.
그리디 알고리즘은 그 순간 가장 좋은 선택지를 고르고 그에 따라 생기는 부분에 대한 부분 문제를 해결한다. 그래서 미래 선택에 영향을 주지만 미래의 선택이나 부분 문제에 의존하지는 않는다. 하향식 접근을 통해서 주어진 문제를 더 작게 만들어가며 문제를 해결하는 방법으로 문제 해결이 진행된다.
그리디와 동적 프로그래밍 모두 모든 케이스를 통해서 최적의 해를 찾는 방법이다. 다만 접근 방식이 다르다. 그리디는 순간의 최적 값을 추가하기 위해 모든 케이스를 특정 조건으로 정렬하여 케이스를 하나씩 확인하며 문제가 해결되는지 확인한다. 그리고 동적 프로그래밍은 큰 조건의 부분 조건을 먼저 해결하여 그 값을 사용함으로서 반복되는 연산을 줄여 전체 케이스를 탐색하는 시간과 속도를 최적화한 방법으로 볼 수 있다.
그리디 알고리즘의 대표적인 문제이다. 그 외에도 최소 신장 트리가 있다.
주어진 동전 종류로 특정 금액을 만들기 위해 필요한 동전의 최소 개수를 구하는 문제다.
이 문제를 해결하기 위해서는 무조건 큰 금액의 동전부터 계산하는 것이 바람직하다.
왜냐하면 큰 금액의 동전을 제외한 나머지 동전으로 같은 금액을 조합해도 무조건 더 많은 개수가 발생할 수 밖에 없다. ( ex. 500원 → 500원 1개, 100원 5개 )