노트

동적 프로그래밍

Dynamic Programming

CS#algorithm · 연결된 개념 7개

쉽게 말하면

동적 프로그래밍은 숙제를 풀다 구한 중간 답을 공책 귀퉁이에 적어 두고, 같은 계산이 또 나오면 다시 풀지 않고 베껴 쓰는 방법이에요. 같은 계산을 수없이 반복하던 피보나치가 금방 끝나요.

비유의 한계 적어 두기만 하면 다 되는 건 아니에요. 같은 부분 문제가 겹치고, 부분 답을 조합해 전체 최적해를 만들 수 있어야 해요. 재귀에 캐시를 단 메모이제이션은 n이 크면 스택이 넘칠 수 있어요.

복잡한 문제를 더 작은 부분 문제로 나눠 풀되, 한 번 푼 부분 문제의 답을 저장해 다시 계산하지 않는 방법. 두 조건이 맞을 때 쓴다.

  • 겹치는 부분 문제(Overlapping Subproblems): 같은 부분 문제가 여러 번 나온다
  • 최적 부분 구조(Optimal Substructure): 부분 문제의 최적해를 조합해 전체 최적해를 만들 수 있다

피보나치로 보기

단순 재귀 fib(n) = fib(n-1) + fib(n-2)는 같은 값을 수없이 다시 계산해 O(2ⁿ)이다. fib(45)만 돼도 눈에 띄게 느리다.

// 메모이제이션: 위에서 아래로(top-down), 재귀 + 캐시
const memo = new Map<number, number>()
function fibMemo(n: number): number {
  if (n <= 2) return 1
  if (!memo.has(n)) memo.set(n, fibMemo(n - 1) + fibMemo(n - 2))
  return memo.get(n)!
}
 
// 타뷸레이션: 아래에서 위로(bottom-up), 반복문 + 표
function fibTable(n: number) {
  const t = [0, 1, 1]
  for (let i = 3; i <= n; i++) t[i] = t[i - 1] + t[i - 2]
  return t[n]
}

둘 다 O(n)이 된다.

  • 메모이제이션(Memoization)은 원래 재귀 구조를 그대로 두고 캐시만 더해 쓰기 쉽다. 하지만 n이 크면 재귀가 깊어져 스택 오버플로가 날 수 있다(재귀)
  • 타뷸레이션(Tabulation)은 작은 문제부터 표를 채운다. 스택을 쓰지 않고, 필요한 칸만 남기면 공간도 줄일 수 있다(피보나치는 변수 두 개면 된다)

배낭 문제(Knapsack Problem), 최장 공통 부분 수열(Longest Common Subsequence, LCS), 편집 거리(Edit Distance), 동전 거스름돈(Coin Change)이 대표 문제다. 부분 문제가 겹치지 않는다면 분할 정복으로 충분하다. 같은 "계산 결과 재사용" 아이디어가 React의 memo·useMemo·useCallback과 캐시 전반(읽기 캐시 전략)에도 있다.

책: 10장. 동적 계획법: diff는 어떻게 만들까

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • 섣부른 최적화

    "섣부른 최적화는 모든 악의 근원이다." 도널드 크누스(Donald Knuth)가 1974년 글 「Structured Programming with go to Statements」에서 쓴 말이다. 원문의 맥락은 작은 효율은 대부분(약 97%)의 경우 잊으라는 것이고, 정말 중요한 3%는 놓치지 말라는 말이 이어진다.

  • 구조적 공유

    구조적 공유(Structural Sharing)는 새 버전의 데이터를 만들 때 바뀌지 않은 부분은 이전 버전의 객체를 그대로 가리키게 하는 기법이다. 바뀐 노드와 그 노드까지 올라가는 경로만 새로 만들고, 나머지 가지는 두 버전이 함께 쓴다. 불변 데이터를 다루면서도 매번 전체를 복사하지 않게 해 준다.

  • 구성요소 줄이기

    셀 수 있는 모든 구성요소(파일, 폴더 깊이, 클래스, 함수, 분기, 변수, 테이블, 필드, 테스트 케이스…)는 비용이라는 관점. 심플 디자인의 네 번째 규칙을 실무 기준으로 풀어낸 것이다.

  • 인지 부하

    작업 기억이 한 번에 처리해야 하는 양. 문제 자체의 복잡함과 표현 방식에서 오는 복잡함으로 나뉜다.

  • 청킹과 코드 읽기

    여러 정보를 의미 있는 덩어리 하나로 묶어 기억하는 것. 아는 것이 많을수록 코드를 큰 덩어리로 읽는다.

보기 옵션