알고리즘/문제해결전략

[8] 동적 계획법

남현경 2018. 8. 7. 15:37

[동적 계획법]

 - 큰 의미에서 분할 정복과 같은 접근 방식

 - 어떤 부분 문제는 두 개 이상의 문제를 푸는데 사용될 수 있기 때문에, 이 문제의 답을 여러 번 계산하는 대신 한 번만 계산하고 계산 결과를 재활용함으로써 속도의 향상을 꾀할 수 있다.

-> 각 문제의 답을 메모리(캐시)에 저장해 둘 필요가 있다! 왜냐하면 중복 호출이 기하급수적으로 증가하기 때문에 계산량의 낭비를 피하기 위해!

    반환 값이 일정하다는 사실을 이용하면 답을 저장하는 캐시 배열을 만들어서 각 입력에 대한 반환 값을 저장할 수 있다.

 

이와 같이 함수의 결과를 저장하는 장소를 마련해 두고, 한 번 계산한 값을 저장해 뒀다 재활용하는 최적화 기법을 메모이제이션이라고 한다.

//-1로 초기화해 둔다.
int cache[30][30];
int bino2(int n, int r){
//기저사례
if(r==0 || n==r) return 1;

//-1이 아니라면 한 번 계산했던 값이니 곧장 반환
if(chche[n][r] != -1)
  return cache[n][r];

return cache[n][r] = bino2(n-1, r-1) + bino2(n-1, r);
}

(메모이제이션을 사용하면 모든 부분 문제가 한 번씩만 계산된다고 보장할 수 있기 때문에 함수 호출 횟수가 엄청나게 감소하리라 예상할 수 있다!)

 

*메모이제이션은 함수의 반환 값이 그 입력 값만으로 결정될 때만 사용가능하다. -> 입력이 같은데 외부 요소에 따라 다른 값이 반환된다면 캐싱 X

 

 

[메모이제이션 구현 패턴]

 

1. 항상 기저 사례를 제일 먼저 처리한다. 

   입력이 범위를 벗어난 경우 등을 기저 사례로 처리하면 매우 유용한데, 기저 사례를 먼저 확인하지 않고 cache[]에 접근하면 범위를 벗어나는 등의       오류가 있을 수 있다.

 

2. 함수의 반환 값이 항상 0 이상이라는 점을 이용해라.

   cache[]를 모두 -1로 초기화하면 -1일 경우 이 값은 계산된 반환 값일리가 없다!

 

3. ret가 cache[a][b]에 대한 참조형이라는데 유의해라.

   참조형 ret의 값을 바꾸면 cache값도 변하기 때문에 저장할 때도 매번 귀찮게 cache라고 쓸 필요가 없어진다.

 

4. memset()을 이용해 cache[]를 초기화해라.

   다중 for문보다 쉽게 초기화할 수 있다!(하지만 굉장히 제한적인 경우에만 쓸 수 있는 일이다. 만약 배열이 32비트나 64비트 정수형일 때는 X)

 

코드 8.3 메모이제이션의 사용 예

//전부 -1로 초기화해 둔다.
int cache[2500][2500];
//반환 값은 항상 int형 안에 들어가는 음이 아닌 정수
int function(int a, int b) {
	//기저 사례를 처음에 처리!
	if (..)	return ..;
	//(a,b)에 대한 값을 구한 적이 있으면 곧장 반환
	int& ret = cache[a][b];
	if (ret != -1)	return ret;
	//여기에서 답 계산
	..
		return ret;
}

int main() {
	memset(cache, -1, sizeof(cache));
}