프로그래밍/알고리즘

[알고리즘] 다이나믹 프로그래밍 개념 정리

Be개발자 2021. 3. 17. 00:54

 

 

다이나믹 프로그래밍은 메모리 공간을 약간 더 사용하여 연산 속도를 비약적으로 증가시키는 방법이다. 다만 항상 사용할 수 있는 것은 아니며, 다음 조건을 만족할 때 사용할 수 있다.

 

1. 큰 문제를 작은 문제로 나눌 수 있다.

2. 작은 문제에서 구한 정답은 그것을 포함하는 큰 문제에서도 동일하다.

 

 

메모이제이션(Memoization)은 다이나믹 프로그래밍을 구현하는 방법 중 한 종류로, 한 번 구한 결과를 메모리 공간에 메모해두고 같은 식을 다시 호출하면 메모한 결과를 그대로 가져오는 기법을 말한다. 메모이제이션은 값을 저장하는 방법이기에 캐싱(Caching)이라고도 한다.

 

구현 방법은 한 번 구한 정보를 리스트에 저장하는 것이다. 다이나믹 프로그래밍을 재귀적으로 수행하다가 같은 정보가 필요할 때는 예전에 구한 정답을 그대로 리스트에서 가져오면 되는 것이다. 

 

 

피보나치 수열의 경우 다이나믹 프로그래밍을 사용하면 복잡한 연산량을 해결할 수 있다.

기존에 우리가 아는 피보나치 수열의 점화식을 토대로 코드를 구현하면 아래와 같다.

 

# 피보나치 함수
def fibo(x):
	if x == 1 or x == 2:
    	return 1
    return fibo(x-1) + fibo(x-2)

print(fibo(4))

 

위 코드의 문제점은 fibo(n)에서 n의 값이 커질수록 수행시간과 연산량이 기하급수적으로 증가한다. 피보나치 수열의 경우 시간 복잡도가 O(2^N)으로 N= 30, 즉 fibo(30)을 구하려 하면 약 10억 가량의 연산을 수행해야 한다. 이를 메모이제이션 기법을 사용하면 연산량이 보다 줄어든다.

 

 

# 한 번 계산한 결과를 메모이제이션하기 위한 리스트 초기화
d = [0] * 100

# 피보나치 함수를 재귀로 구현(탑다운 다이나믹 프로그래밍)
def fibo(x):
	# 종료 조건
    if x == 1 or x == 2:
    	return 1
    # 이미 계산한 적 있는 문제라면 그대로 반환
    if d[x] != 0:
    	return d[x]
    # 아직 계산하지 않은 문제라면 점화식에 따라서 피보나치 결과 반환
    d[x] = fibo(x-1) + fibo(x-2)
    return d[x]
    
print(fibo(99))

 

 

위와 같이 다이나믹 프로그래밍을 적용했을 때의 피보나치 수열 알고리즘의 시간 복잡도는 O(N)이다. 한 번 구한 결과는 다시 구해지지 않기 때문이다. 이처럼 재귀 함수를 이용하여 다이나믹 프로그래밍 소스코드를 작성하는 방법을, 큰 문제를 해결하기 위해 작은 문제를 호출한다고 하여 탑다운(Top-Down) 방식이라고 한다. 

 

재귀함수 대신 반복문을 사용하는 경우가 있는데 반복문을 사용하여 소스코드를 작성하는 경우 작은 문제부터 차근차근 답을 도출하여 바텀업(Bottom-Up) 방식이라고 말한다. 

피보나치 수열을 Bottom-Up 방식으로 구현하는 방법은 아래와 같다.

# 피보나치 수열 (바텀업 방식)

# DP 테이블 초기화
d = [0] * 100

# 첫 번째 피보나치 수와 두 번째 피보나치 수는 1
d[1] = 1
d[2] = 1

n = 99

# 피보나치 함수를 반복문으로 구현(바텀업 방식)
for i in range(3, n-1):
	d[i] = d[i-1] + d[i-2]
    
print(d[n])

 

메모이제이션은 때에 따라 다른 자료형을 이용할 수도 있다. 사전 자료형을 예시로 들 수 있는데 사전 자료형은 수열처럼 연속적이지 않은 경우에 유용하다. 코딩테스트에서 다이나믹 프로그래밍 문제는 대체로 간단한 형태로 출제된다. 특정 문제를 완전 탐색으로 접근했을 때 시간이 오래 걸리면 다이나믹 프로그래밍을 적용할 수 있는지 해결하고자 하는 부분 문제들의 중복 여부를 확인해봐야 한다.

 

또한 재귀함수로 비효율적인 프로그램을 작성하고 나중에 탑다운 방식처럼 작은 문제에서 구한 답이 큰 문제에서도 사용될 수 있으면 재귀 함수를 메모이제이션을 이용하여 코드 수정을 하는 것도 좋은 방법이다. 

 

재귀함수를 이용하는 탑다운 방식보다는 바텀업 방식으로 구현하는 것을 권장한다. 시스템 상 재귀함수의 스택 크기가 한정되어 있을 수 있기 때문이다.