티스토리 뷰
프로그래밍 문제해결에서 가장 많이 쓰이는 코딩 스킬 중 하나인 "동적 계획법(Dynamic Programming)"에 대해 알아볼 시간이다. 이름에서 알 수 있듯이 알고리즘이 아는 프로그래밍 방법이라고 생각하면 좋다. 하지만, 알고리즘 시간에 소개하는 이유는 워낙 많이 쓰이기도 하고, 앞서 배웠던 Conquer 알고리즘의 응용 버전이기 때문이기도 해서 이번 시간에 소개를 할 생각이다.
우리 모두 피보나치 수열이 무엇인지는 알고 있을 것이다. 피보나치 문제를 푸는 가장 좋은 방법은 반복해서 풀어낸다. 코드로 작성하기도 매우 쉽고 직관적으로도 이해가 잘 된다. 피보나치 수열을 recursive 하게 풀어낸다면 아래와 같은 코드를 작성하게 될 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import java.util.Scanner; public class Solution { public static void main(String[] args) { // TODO Auto-generated method stub Scanner sc = new Scanner(System.in); int a = sc.nextInt(); System.out.println(fibo(a)); } static int fibo(int a){ if (a==1||a==0) return 1; return fibo(a-1)+fibo(a-2); } } | cs |
코드도 20줄 이내로 작성 될 만큼 매우 간단하고, 직관적으로도 이해가 빠를 것이다. 하지만, 재귀 방식의 단점은 무엇일까? 함수를 연속적으로 불러야 하기 때문에 컴파일러는 해당 코드를 읽을 때 수많은 오버헤드가 발생될 것이다. 숫자가 작다면 큰 의미가 없겠지만, 네자리, 다섯 자리 넘어가게 된다면 메소드를 부르기 위해 Stack에 명령이 쌓일 것이고, 쉽게 Stack Overflow가 발생한다. 이 문제를 해결하기 위해 고안된 방법이 바로, 동적 계획법이다.
1. 동적 계획법이란?
해당 fibo의 수를 구하기 위해서 일정 공간을 지는 배열에 해당 값을 입력한다. 일정 공간을 총 50개라고 가정을 하자. 그리고, 50개에 해당 하는 값을 반복문을 활용해 저장한 뒤 구하고자 하는 항이 몇인 지를 출력해 내는 방식이다.
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 31 32 33 34 35 36 37 38 39 40 | import java.util.Scanner; public class Solution { static int[] fibo_memo; public static void main(String[] args) { // TODO Auto-generated method stub Scanner sc = new Scanner(System.in); int a = sc.nextInt(); fibo_memo = new int[45]; fibo(a); } static void fibo(int a){ fibo_memo[0]=0; fibo_memo[1]=1; for(int i=2; (i<=a)&&(i<45); i++){ fibo_memo[i]=fibo_memo[i-2]+fibo_memo[i-1]; } System.out.println(fibo_dp(a)); } static int fibo_dp(int a){ if(a<45) return fibo_memo[a]; else{ return fibo_dp(a-1)+fibo_dp(a-2); } } } | cs |
fibo_memo라는 배열을 생성하여 피보나치 함수 값을 미리 구하여 저장하였다. 그 크기를 45로 지정하였는다. Java 컴파일러에서 허용된 int의 범위값을 저장 할 수 있는 범위가 피보나치 수열의 45항이기 때문에 위와 같이 설정했다. 만약 이보다 더 큰수를 구하는 프로그래밍이 필요하다면 변수 값을 int가 아닌 다른 변수로 지정해야한다. 이런 과정으로 동적 계획법을 이용한 피보나치 수열 프로그램을 만들었다. 재귀 함수로 푼 피보나치의 시간 복잡도는 O(2^n)이다. 재귀 함수를 이용한 방법의 도표를 그린다면 시간 복잡도가 왜 저렇게 나오는지 이해하기가 쉽다. 3인 경우 총 8번의 연산을, 4인 경우 총 16번의 연산을 실시한다. 반면 동적 계획법의 시간 복잡도는 O(n^2)이다.
동적 계획법은 연산을 크게 줄이는데 매우 유리하나, 그만큼의 불리함도 2가지 존재한다. 첫 번째는 한정적인 공간이다. 재귀 방식은 컴파일러가 매우 뛰어나고 연산 속도가 빠르고 Stack Overflow를 막을 수 있다면 무한대로 원하는 결과 값을 얻어낼 수 있다. 값을 입력하고 곧바로 구해내기 때문에 아무리 큰 숫자가 와도 문제를 해결할 수 있다. 물론, 이 세상에 그런 컴퓨터는 존재하지 않는다. 그리고, 추가 공간이 필요하다는 점이다. 메모리가 필요없는 재귀 방식에 비해, DP는 추가 공간이 필요하다는 불편함이 존재한다.
2. 거스름돈 문제
이번에는 Java 언어를 활용해 해당 알고리즘을 구현해보자.
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | import java.util.Scanner; public class Solution { static int[] change; public static void main(String[] args) { // TODO Auto-generated method stub Scanner sc = new Scanner(System.in); change = new int[100]; System.out.println("거스름돈을 입력해주세요:"); int i = sc.nextInt(); calc_change(); System.out.println("지불해야하는 거스름돈 개수 :" + change[i]); } static void calc_change(){ change[0]=0; for(int i=1; i<100; i++){ change[i] = min_change(i)+1; } } static int min_change(int i){ int min = change[i-1]; if(i>=3){ if(min>change[i-3]) min = change[i-3]; } if(i>=4){ if(min>change[i-4]) min = change[i-4]; } return min; } } | cs |
동전에 1,3,4원이 존재한다고 가정하고, 배열의 크기를 100으로 해 100원까지의 거스름돈의 개수를 위와 같은 프로그래밍으로 쉽게 구할 수 있다. 공간의 크기가 100인 배열이 존재한다면 값을 쉽게 구해낼 수 있다.
3. Knapsack 문제
아이템 1번 7kg, 2번 3kg, 3번 4kg, 4번 5kg라고 했을 때, Item 행은 물건을 하나씩 추가 하면서 최대 무게 Weight일 때 나오는 최대의 무게를 구하는 방법이다. F(i,j)라고 할 때 F(2,7)은 가방에 7kg의 물건을 담았고, 상품으로 7,3kg 두개의 물건을 사용 했을 때 구할 수 있는 최대 값이다. 아이템 하나를 추가하기 전과 이전의 값을 비교하여 큰 값을 Table에 채우는 방법으로 문제를 해결 할 수 있다. 해당 알고리즘은 아래와 같은 Pseudo Code 로 표현 가능하다.
'Skill > Programming - Basic' 카테고리의 다른 글
14. 탐욕 알고리즘 (Greedy Algorithm) - 1 (0) | 2018.07.30 |
---|---|
13. 최단거리 문제 (0) | 2018.07.15 |
11. 문자열 찾기 (String Matching Algorithm) (0) | 2018.05.24 |
10. 시간-공간 변환 (Space-Time trade off) (0) | 2018.05.09 |
9. 변환 정복 알고리즘 (Transform & Conquer) (0) | 2018.04.27 |