1_[algorithm]dynamic programming & divide and conquer
동적 계획법과 분할정복
Dynamic Programming(DP) and ‘Divide and Conquer’
- 어떤 문제를 풀기위한 전략!
- 동적계획법 DP
- 입력크기가 작은 부분 문제들을 해결한 후, 해당 부분 문제의 해를 활용해서 보다 큰 크기의 부분 문제를 해결함으로써 최종적으로 전체 문제를 해결하는 알고리즘
- 상향식 접근법으로, 가장 최하위 해답을 구한 후, 이를 저장하고 , 해당 결과값을 이용해서 상위 문제를 풀어가는 방식
- Memoization(메모이제이션) 기법 사용
- 프로그램 실행 시 이전에 계산한 값을 저장하여, 다시 계산하지 않도록 함으로써 전체 실행 속도를 빠르게 하는 기술
- 문제를 잘게 쪼갤 때, 부분 문제는 중복되어 재활용됨 -예: 피보나치 수열
2..분할 정복 Divide and Conquer
- 문제를 나눌 수 없을 때까지 나누어서 각각을 풀면서 다시 합병하여 문제의 답을 얻는 알고리즘
- 하향식 접근법으로, 상위의 해답을 구하기 위해 아래로 내려가면서 하위의 해답을 구하는 방식 -일반적으로 재귀함수로 구현
- 문제를 잘게 쪼갤 때, 부분 문제는 서로 중복되지 않음
- 메모이제이션을 사용하지 않음
📌 공통점과 차이점 📌
- 공통점
- 문제를 잘게 쪼개서 가장 작은 단위로 분할
2.차이점
a.동적계획법
- 부분 문제는 중복되어, 상위 문제 해결시 재활용됨
- 메모이제이션 기법 사용(부분 문제의 해답을 저장해서 재활용하는 최적화 기법으로 사용)
- 가장 최하위 해답을 최우선으로 함
b.분할정복
- 부분문제는 서로 중복되지 않음
- 메모이제이션 기법을 사용하지 않음
- 상위의 해답을 우선으로 함
🌹 동적 계획법 알고리즘 이해 - 예시1. 피보나치 수열
source: http://jwilson.coe.uga.edu/EMAT6680Su07/Brown/Assignment 12/Fibonnaci Sequence.htm
먼저 몇 개 입력에 대해서만 연습해보자
f(0)=0
f(1)=1
f(2)=1
f(3)=2
f(4)=3
f(5)=5
f(6)=8
f(7)=13
f(8)=21
f(9)=34
피보나치 수열은 위와 같이 n 입력에 따라 다른 케이스로 구분된다
✴️ 음.. 그런데 이렇게 보니 재귀용법으로도 접근할 수 있을 것 같은데..?
➡️ 재귀용법과 동적계획법으로 두가지 방법으로 접근해보자!
- 재귀용법으로 접근
package dpConquer;
public class Fibonacci {
//재귀용법으로 접근
public static int fiboRecur(int n){
if(n<=1){
return n;
}
return fiboRecur(n-2)+fiboRecur(n-1);
}
public static void main(String[] args){
System.out.println("fiboRecure(0): "+fiboRecur(0));
System.out.println("fiboRecure(1): "+fiboRecur(1));
System.out.println("fiboRecure(2): "+fiboRecur(2));
System.out.println("fiboRecure(3): "+fiboRecur(3));
/*
* fiboRecure(0): 0
fiboRecure(1): 1
fiboRecure(2): 1
fiboRecure(3): 2
*
* */
}
}
재귀용법으로 접근하면 위와 같이 케이스를 나누어서 메서드를 call하도록 하면 될 것이다
- 동적 계획법으로 접근
1.먼저 입력으로 들어오는 것을 “배열의 요소 갯수”라고 생각하고
그 크기만큼 배열을 만들자
public static int fiboDp(int n){
//n개 만큼의 배열을 만들기
Integer[] cache=new Integer[n+1];
}
2.그리고, 0일때와 1일때와 같은 케이스를 미리 넣어주자
public static int fiboDp(int n){
//n개 만큼의 배열을 만들기
Integer[] cache=new Integer[n+1];
//미리 예외 케이스를 넣어두기
if(n==0){
**cache[0]=0;**
}else if(n==1){
**cache[0]=0;**
**cache[1]=1;**
}
}
3.그리고 그 이후, 즉 인덱스 2부터는 f(n-1)+f(n-2)의 규칙이 적용되므로 이에 대해서는 for 루프를 이용해서 반복해서 넣어주자
public static int fiboDp(int n){
//n개 만큼의 배열을 만들기
Integer[] cache=new Integer[n+1];
//미리 예외 케이스를 넣어두기
if(n==0){
cache[0]=0;
}else if(n==1){
cache[0]=0;
cache[1]=1;
}else{
cache[0]=0;
cache[1]=1;
//그 이후를 넣어주기
for(int i =2; i < n+1; i++){
**cache[i]=cache[i-2]+cache[i-1];**
}
}
}
▶️2,3의 과정을 통해서 가장 작은 값을 이용해서 그에 대한 상위값을 계산할 수 있다는 것을 알 수 있다!
4.그러면, 결론적으로 cache[n], 즉 최상위값을 리턴해주면 된다
public static int fiboDp(int n){
//n개 만큼의 배열을 만들기
Integer[] cache=new Integer[n+1];
//미리 예외 케이스를 넣어두기
if(n==0){
cache[0]=0;
}else if(n==1){
cache[0]=0;
cache[1]=1;
}else{
cache[0]=0;
cache[1]=1;
//그 이후를 넣어주기
for(int i =2; i < n+1; i++){
cache[i]=cache[i-2]+cache[i-1];
}
}
**return cache[n];**
}
▶️ 재귀용법은 각각의 값을 저장하지 않기 때문에 필요한 값들을 그때마다 계산하게 된다는 차이가 있다!
- 위에서 if 로 분기해둔 이유는, 0이나 1일 경우 인덱스에 대해서 java.lang.ArrayIndexOutOfBoundsException 이 발생할 수 있기 때문에 나눠둔 것이다!
- 분할정복은 지금 상태에서는 개념적으로만 이해해보고, 이후 병합정렬과 퀵정렬에서 알아보도록 하자