[다이나믹 프로그래밍] 개념 정리
다이나믹 프로그래밍(동적 계획법)?!
- 메모리를 적절히 사용하여 수행 시간 효율성을 비약적으로 향상시키는 방법임
- 이미 계산된 결과(작은 문제)는 별도의 메모리 영역에 저장하여 다시 계산하지 않도록 함
- 구현은 일반적으로 2가지 방식(탑다운 & 보텀업)으로 구성됨
- '동적 계획법' 이라고도 부르는데,
- 흔히 프로그래밍에서 다이나믹은 '프로그램이 실행되는 도중에' 라는 의미임
- 예를 들어, 자료구조에서 동적 할당은 프로그램 실행 중에 프로그램 실행에 필요한 메모리를 할당하는 기법임
- 하지만 다이나믹 프로그래밍에서의 '다이나믹'은 동적 할당(Dynamic Allocation)에서의 다이나믹과는 다른 의미임
다이나믹 프로그래밍의 기본적인 아이디어
1줄 요약 : 다이나믹 프로그래밍이란 큰 문제를 작게 나누고, 같은 문제라면 한 번씩만 풀어 문제를 효율적으로 해결하는 알고리즘 기법
# 다이나믹 프로그래밍을 사용하기 위한 조건
- 주어진 문제가 다음의 조건을 만족할 때 사용할 수 있음
1. 최적 부분 구조 (Optimal Substructure)
→ 큰 문제를 작은 문제로 나눌 수 있으며 작은 문제의 답을 모아서 큰 문제를 해결할 수 있음
2. 중복되는 부분 문제 (Overlapping Subproblem)
→ 동일한 작은 문제를 반복적으로 해결해야 함
# 구현 방법
## 메모이제이션(Memoization) - 개념적 용어
- 메모이제이션은 다이나믹 프로그래밍을 구현할 때 사용하는 방법임(중복되는 부분 문제 해결을 위한 방법 중 하나)
- 한 번 계산한 결과를 메모리 공간에 메모하는 기법
- 같은 문제를 다시 호출하면 메모했던 결과를 그대로 가져옴
- 값을 기록해 놓는다는 점에서 캐싱(Caching)이라고도 함 (컴퓨터 구조 수업에서도 지금 Cache Memory에 대해 공부하고 있는데, 알고리즘에서도 캐싱 기법이 적용될 수 있다니..! 흥미롭다😲)
## 탑다운 VS. 보텀업
- 탑다운 방식은 하향식이라고도 하며, 보텀업 방식은 상향식이라고도 함
- 다이나믹 프로그래밍의 전형적인 형태는 보텀업 방식임
- 결과 저장용 리스트는 DP 테이블이라고 부름
- 엄밀히 말하면, 메모이제이션은 이전에 계산된 결과를 일시적으로 기록해 놓은 넓은 개념을 의미함
- 메모이제이션은 다이나믹 프로그래밍에 국한된 개념이 아님
- 한 번 계산된 결과를 담아 놓기만 하고 다이나믹 프로그래밍을 위해 활용하지 않을 수도 있음
## 탑다운(Top-Down) 방식
→ 재귀 함수를 이용하여 다이나믹 프로그래밍 소스코드를 작성하는 방법
→ 큰 문제를 해결하기 위해 작은 문제를 호출한다고 하여 탑다운 방식이라고 함 (하향식)
## 보텀업(Bottom-Up) 방식
→ 반복문을 이용하여 다이나믹 프로그래밍 소스코드를 작성하는 방법
→ 작은 문제부터 차근차근 답을 도출함 (상향식)
다이나믹 프로그래밍으로 해결할 수 있는 예시 - 피보나치 수열
# 기존 구현 방식
def fibo(x):
if x == 1 or x == 2:
return 1
return fibo(x-1) + fibo(x-2)
print(fibo(50))
문제점 :
fibo(x) 함수에 x 가 커질수록 수행 시간이 기하급수적으로 늘어남
동일한 함수가 반복적으로 호출됨 → 이미 한 번 계산했지만, 계속 호출할 때마다 계산함
# 다이나믹 프로그래밍 적용 방식
구현1) 메모이제이션 기법을 사용한 피보나치 수열 코드 by 탑다운(Top-Down) 방식 - 재귀적
import java.util.*;
public class fibonacci_with_memoization {
// 한 번 계산된 결과를 메모이제이션(Memoization)하기 위한 배열 초기화
public static long[] result = new long[100];
// 피보나치 함수를 재귀함수로 구현 (with 탑다운 다이나믹 프로그래밍)
public static long fibo(int x){
if(x == 1 || x == 2){ // 재귀 종료 조건
return 1;
}
if(result[x] != 0){ // 이미 계산한 적이 있는 값이라면 배열의 값 그대로 반환
return result[x];
}
result[x] = fibo(x-1) + fibo(x-2); // 계산한 적이 없는 겂이라면 점화식에 따라 피보나치 결과 반환
return result[x];
}
public static void main(String[] args){
System.out.println(fibo(50));
}
}
→ 재귀적으로 수행하다가 같은 정보가 필요할 때는 이미 구한 정답을 그대로 리스트에서 가져오면 됨
구현2) 메모이제이션 기법을 사용한 피보나치 수열 코드 by 보텀업(Bottom-Up) 방식 - 반복적
import java.util.*;
public class fibonacci_with_memoization_iterative {
public static long[] result = new long[1000000];
public static void main(String[] args){
// 첫 번째와 두 번째 값은 1로 고정
result[1] = 1;
result[2] = 1;
int val = 100; // 찾고자 하는 값
// 피보나치 함수를 반복문으로 구현(다이나믹 프로그래밍 보텀업 방식)
for(int i = 3; i <= val; i++){
result[i] = result[i-1] + result[i-2];
}
System.out.println(result[val]);
}
}
→ 시간 복잡도 : O(N)
→ 이유 : f(1)을 구한 다음 그 값이 f(2)를 푸는데 사용되고, f(2)의 값이 f(3)을 푸는데 사용되는 방식으로 이어짐. 즉 한 번 구한 결과는 다시 구해지지 않음
다이나믹 프로그래밍 VS. 분할 정복
- 모두 최적 부분 구조를 가질 때 사용 가능함
- 큰 문제를 작은 문제로 나눌 수 있으며 작은 문제의 답을 모아서 큰 문제를 해결할 수 있는 상황
- 차이점은 부분 문제의 중복
- 다이나믹 프로그래밍 문제에서는 각 부분 문제들이 서로 영향을 미치며 부분 문제가 중복됨
- 분할 정복 문제에서는 동일한 부분 문제가 반복적으로 계산되지 않음
다이나믹 프로그래밍 문제에 접근하는 방법
# 주어진 문제가 다이나믹 프로그래밍 유형인지 파악하기
- 주어진 문제가 다이나믹 프로그래밍 유형임을 파악하는 것이 중요함
- 가장 먼저 그리디, 구현, 완전 탐색 등의 아이디어로 문제를 해결할 수 있는지 검토할 수 있음
- 다른 알고리즘으로 풀이 방법이 떠오르지 않으면 다이나믹 프로그래밍을 고려해 보자!
- 특정한 문제를 완전 탐색 알고리즘으로 접근했을 때 시간이 매우 오래 걸리면 다이나믹 프로그래밍을 적용할 수 있는지 즉 해결하고자 하는 부분 문제들의 중복 여부를 확인해봐야 함
# 비효율적인 코드 개선하기
- 일단 재귀 함수로 비효율적인 완전 탐색 프로그램을 작성한 뒤에 (탑다운) 작은 문제에서 구한 답이 큰 문제에서 그대로 사용될 수 있으면, 코드를 개선하는 방법을 사용할 수 있음
- 가능하다면 재귀 함수로 먼저 구현하기보다 보텀업 방식(반복문)으로 구현하는 것이 좋음 (이유: 시스템상 재귀 함수의 스택 크기가 한정되어 있을 수 있음)
- 일반적인 코딩 테스트 수준에서는 기본 유형의 다이나믹 프로그래밍 문제가 출제되는 경우가 많음
참고) 이것이 취업을 위한 코딩 테스트다 with 파이썬