동적 프로그래밍 (Dynamic Programming)

CS/알고리즘

같은 계산을 반복하는 것은 비효율적이다. 예를 들어 피보나치 수열에서 fib(5)를 계산할 때, fib(3)을 여러 번 중복 계산한다.
한 번 계산한 결과를 저장해두면 훨씬 빠를 것이다.

이것이 바로 동적 프로그래밍(Dynamic Programming)의 핵심 개념이다.

 

동적 프로그래밍 (Dynamic Programming)

동적 프로그래밍은 복잡한 문제를 작은 부분 문제로 나누어 해결하고, 그 결과를 저장하여 중복 계산을 방지하는 알고리즘 기법이다.

동적 프로그래밍은 크게 두 가지 방식으로 구현할 수 있다.

  메모이제이션 타뷸레이션
방식 하향식 (Top-down) 상향식 (Bottom-up)
구현 재귀 + 캐싱 반복문 + 테이블
계산 필요한 값만 계산 모든 값을 미리 계산
메모리 스택 + 캐시 공간 테이블 공간만

문제: 피보나치 수열

피보나치 수열을 예시로 두 방식을 비교해보자.
피보나치 수열: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...

  • fib(0) = 0
  • fib(1) = 1
  • fib(n) = fib(n-1) + fib(n-2)

 

일반적인 재귀 (비효율적)

먼저 동적 프로그래밍 없이 단순 재귀로 구현해보자.

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

  let start = new Date();
  console.log(fibonacci1(40));  // 102334155

  let end = new Date();
  console.log(`실행시간: ${end - start}ms`);  //  실행시간: 980ms
}

 

문제점은 fib(5)를 계산할 때:

fib(5)
= fib(4) + fib(3)
= (fib(3) + fib(2)) + (fib(2) + fib(1))
= ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

fib(3)이 2번, fib(2)가 3번 중복 계산된다. 시간복잡도는 O(2^n) 으로 매우 비효율적이다.

 

메모이제이션 (Memoization)

계산 결과를 저장해서 여러 번 계산하지 않도록 하는 기법이다.
하향식(Top-down) 방식으로, 큰 문제를 작은 문제로 나누며 필요한 값만 계산한다.

구현 코드

function fibonacci2(n, memo) {
  if (n == 0 || n == 1) return n;

  // 이미 계산한 값이 있으면 재사용
  if (memo[n] == null) {
    memo[n] = fibonacci2(n - 2, memo) + fibonacci2(n - 1, memo);
  }

  return memo[n];
}

let start = new Date();
console.log(fibonacci2(40, {}));  // 102334155
let end = new Date();
console.log(`실행시간: ${end - start}ms`);  // 실행시간: 1ms

980ms → 1ms로 엄청난 성능 향상을 확인할 수 있다.

 

장점

  • 재귀 덕분에 하향식 계산 방식으로 어려운 문제를 간단하게 해결할 수 있음
  • 중복 계산을 하지 않아서 속도가 빠름
  • 필요한 값만 계산하므로 불필요한 계산을 하지 않음

단점

  • 재귀 호출로 인한 스택 메모리 사용
  • 메모이제이션을 위한 추가 메모리(해시 테이블) 필요

 

타뷸레이션 (Tabulation)

작은 문제부터 시작해 점진적으로 큰 문제를 해결하는 상향식(Bottom-up) 계산 방식이다.
재귀 없이 반복문으로 구현하며, 모든 값을 미리 계산해 테이블에 저장한다.

구현 코드

function fibonacci3(n) {
  if (n == 0 || n == 1) return n;

  let table = [0, 1];  // 초기값 설정

  // 작은 문제부터 순서대로 계산
  for (let i = 2; i <= n; i++) {
    table[i] = table[i - 2] + table[i - 1];
  }

  return table[n];
}

let start = new Date();
console.log(fibonacci3(40));  // 102334155
let end = new Date();
console.log(`실행시간: ${end - start}ms`);  // 실행시간: 0ms

1ms → 0ms으로 메모이제이션보다도 빠르다.

 

동작 과정

fib(%)를 계산한다고 가정해보자.

 

초기: table = [0, 1]

 

i = 2:

table[2] = table[0] + table[1] = 0 + 1 = 1
table = [0, 1, 1]

i = 3:

table[3] = table[1] + table[2] = 1 + 1 = 2
table = [0, 1, 1, 2]

i = 4:

table[4] = table[2] + table[3] = 1 + 2 = 3
table = [0, 1, 1, 2, 3]

i = 5:

table[5] = table[3] + table[4] = 2 + 3 = 5
table = [0, 1, 1, 2, 3, 5]

 

결과: 5

 

장점

  • 상향식 계산 방식으로 모든 값을 전부 계산 후 테이블에 저장
  • 재귀 호출 없이 반복문을 사용해 스택 오버플로우 위험이 없음
  • 메모리 사용량이 적고 실행 속도가 빠름

단점

  • 필요 없는 값까지 모두 계산할 수 있음
  • 재귀에 비해 직관적이지 않을 수 있음

 

메모이제이션 vs 타뷸레이션

성능 비교

  실행 시간 메모리 사용
일반 재귀 980ms 적음 (스택만)
메모이제이션 1ms 많음 (스택 + 해시 테이블)
타뷸레이션 0ms 보통 (테이블만)

 

어느 것을 선택해야 할까?

상황에 따라 적합한 방식이 다르다.


메모이제이션이 유리한 경우

  • 복잡한 문제를 직관적으로 해결할 수 있는 재귀적 구조가 명확할 때
  • 하향식 접근이 문제 해결에 더 자연스러울 때
  • 모든 부분 문제를 계산할 필요 없이 필요한 값만 계산하면 되는 경우

메모이제이션은 재귀의 직관성을 유지하면서 중복 계산을 제거해 성능을 크게 개선한다. 더 많은 메모리를 사용하지만, 복잡한 문제를 쉽게 해결할 수 있다는 장점이 있다.

 

타뷸레이션이 유리한 경우

  • 재귀적 접근이 직관적이지 않거나 불필요할 때
  • 모든 부분 문제의 해가 필요한 경우
  • 메모리 효율과 실행 속도 모두 중요한 경우
  • 스택 오버플로우 위험을 피하고 싶을 때

타뷸레이션은 상향식으로 필요한 모든 값을 미리 계산해 테이블에 저장하므로, 적은 메모리로 더 빠른 성능을 낼 수 있다.

 

결론적으로, 분할 정복 문제에서 재귀가 직관적이라면 메모이제이션을, 그렇지 않다면 타뷸레이션을 선택하는 것이 좋다.

 

동적 프로그래밍의 활용 사례

동적 프로그래밍은 다음과 같은 문제에서 자주 사용된다.

  1. 피보나치 수열: 가장 기본적인 예제
  2. 최단 경로 문제: 다익스트라, 플로이드-워셜 알고리즘
  3. 배낭 문제(Knapsack): 제한된 용량에서 최대 가치 찾기
  4. 최장 공통 부분 수열(LCS): 두 문자열의 공통 부분 찾기
  5. 동전 교환 문제: 최소 동전 개수로 거스름돈 만들기

 

동적 프로그래밍은 복잡한 문제를 작은 부분 문제로 나누어 해결하는 강력한 기법이다.

메모이제이션(하향식)과 타뷸레이션(상향식) 두 가지 방식이 있으며, 문제의 특성에 따라 적합한 방식을 선택해야 한다. 중복 계산을 제거하여 O(2^n)의 지수 시간을 O(n)으로 줄일 수 있어, 최적화 문제 해결에 필수적인 알고리즘이다.

'CS > 알고리즘' 카테고리의 다른 글

이진 탐색 (Binary Search)  (0) 2026.02.04
P-NP  (0) 2025.11.18
정렬 (Sort)  (0) 2025.11.17
재귀 (Recursion)  (0) 2025.11.17
'CS/알고리즘' 카테고리의 다른 글
  • 이진 탐색 (Binary Search)
  • P-NP
  • 정렬 (Sort)
  • 재귀 (Recursion)
고견
고견
개발 자국 남기기
  • 고견
    개발자국
    고견
  • 전체
    오늘
    어제
    • 분류 전체보기 (157)
      • Frontend (29)
        • Next.js (16)
        • JavaScript (7)
      • CS (19)
        • 자료구조 (9)
        • 알고리즘 (5)
        • 운영체제 (4)
        • 네트워크 (1)
      • TIL (93)
      • Dev Log (16)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    emotion diary
    typescript
    트러블 슈팅
    algorithm
    자료구조
    타입 좁히기
    useState
    인터페이스
    배열
    CS
    cs50
    앱 라우터
    generic
    문자열
    ai 감성 일기장
    바닐라 자바스크립트
    제네릭
    react
    클래스
    페이지 라우터
    함수 타입
    Spa
    C
    Trouble Shooting
    알고리즘
    memory
    javascript
    App Router
    Next.js
    Pages Router
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
고견
동적 프로그래밍 (Dynamic Programming)
상단으로

티스토리툴바