Dynamic Programming

Vorhandenheit ·2021년 12월 10일
0

Dynamic Programming (동적 계획법)

주어진 문제를 여러 개의 하위 문제로 나누어 풀고, 하위 문제들의 해결 방법을 결합하여 최종 문제를 해결하는 문제 해결 방식을 말합니다.

조건

  • 큰 문제를 작은 문제로 나눌 수 있고, 작은 문제가 중복해서 발견된다 (Overlapping Sub-problems)
  • 작은 문제에서 구한 정답은 그것을 포함하는 큰 문제에서도 같다. 작은 문제에서 구한 정답을 큰 문제에서도 사용할 수 있다.(Optimal Substructure)

예시

기존의 피보나치 함수의 답을 구할려고했다면 이렇게 작성했을 테지만,

function fibo(n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return fibo(n - 1) + fibo(n - 2);
}

중복되는 값을을 매번 일일히 계산함으로 시간복잡도가 증가합니다. 이 경우에 시간 복잡도는 O(n^2) 입니다.
이 중복을 제거하기 위해서 메모이제이션(Memoization)이라는 테크닉을 사용합니다.

function fibo(n) {
    memo = [0, 1];
    for (let i = 0; i <= n; i++) 
        memo[i] = memo[i - 1] + memo[i - 2];
    return memo[n];
}

계산한 값을 배열해 저장해서 재활용합니다.

방법

Dynamic Programming 에는 2가지 방법이 있습니다.

  • Top-Down 방식 : 하향식 방법, 가장 큰 문제를 방문하고 작은 문제를 호출하며 답을 찾는 방식 (Memoization + recursive)
function fibMemo(n, memo = []) {
		// 이미 해결한 하위 문제인지 찾아본다
    if(memo[n] !== undefined) {
      return memo[n];
    }
    if(n <= 2) {
      return 1;
    }
		// 없다면 재귀로 결괏값을 도출하여 res 에 할당
    let res = fibMemo(n-1, memo) + fibMemo(n-2, memo);
		// 추후 동일한 문제를 만났을 때 사용하기 위해 리턴 전에 memo 에 저장
    memo[n] = res;
    return res;
}
  • Bottom-Up 방식 : 상향식 방법으로, 작은 문제들로 답을 구해가며 전체 문제의 답을 찾는 방식입니다.(Itertaion + Tabulation)
function fibTab(n) {
    if(n <= 2) {
    	return 1;
    }
    let fibNum = [0, 1, 1];
    for(let i = 3; i <= n; i++) {
        fibNum[i] = fibNum[i-1] + fibNum[i-2];
		// n >= 3 부터는 앞서 배열에 저장해 놓은 값들을 이용하여
		// n번째 피보나치 수를 구한 뒤 배열에 저장 후 리턴한다 
    }
    return fibNum[n];
}

예시

coinChange

  • naive : O(2^M * N)
const coinChange = function (total, coins) {
	const makeChange = (left, idx) => {
    	if (total === 0) { return 1};
      	else {
        let cnt = 0;
          for (let i = idx; i < coins.length; i++) {
          	cnt = cnt + makeChange(left - coins[i], i);
          }
        }
      return cnt;
    } //left에는 total 값이 들어갑니다.
}return makeChange(total, 0);

coinChange가 결국 return 하는 값은 cnt = cnt + cnt + cnt...1 입니다.

  • simple recursion and dp with memoization: O(M * N)
const coinChange = function (total, coins) {
	const memo = [] // memoization 기록할 곳
    for (let i = 0; i < total +1; i++) {
    	memo.push(Array(coins.length).fill(-1))
    } // -1로 채운 coins 수만큼 배열을 memo에 만듭니다
  	const makeChange = (left, idx) => {
    if (left === 0) { return 1} ; // 필요충족조건에 도달했을 때!
    else if (left < 0) { return 0 } ; // total 금액보다 더 많이 빼버린 경우, 조건을 충족하지못해서 횟수로 계산하지않습니다.
   else if (idx >= coins.length && left > 0) return 0 // toal 금액보다 모지란 경우 역시도, 조건을 충족하지못했기 때문에 횟수로 인정하지않습니다.
    }
    else if (memo[left][idx] !== -1) { return memo[left][idx]};
  // 해결하지못한 문제는 다시 풀지 않습니다.
	memo[left][idx] = makeChange(left, idx+1) + makeChange(left - coins[idx], idx);
	}
return makeChange(total, 0)
}
  • dynamic programming with tabulation: O(M * N) (bottom up)
const coinChange = function (total, coins) {
	const table = []
    for (let i = 0; i < total+1; i++) {
    	table.push(Array(coins.length).fill(0))
    }
  	for (let i = 0; i < coins.length; i++) {
    	table[0][i] = 1;
    }
  	for (let amount = 1; amount <= total; amount++) {
    	for (let idx = 0; idx < coins.length; i++) {
        let coinInclude = 0;
          if (amount - coins[idx] >= 0) {
          	coinInclude = table[amount - coins[idx]][idx];
          }
          let coinExclude = 0;
          if (idx >= 1) {
          	coinExclude = table[amount][idx -1];
          }
          table[amount][idx] = coinInclude + coinExclude
        }
    }
  return table[total][coins.length -1]
}
profile
읽고 기록하고 고민하고 사용하고 개발하자!

0개의 댓글

관련 채용 정보