Algorithm|Javascript Algorithm

기억력 좋은 알고리즘의 비밀

0

크리스는 알고리즘 공부를 하다가 유명한 수열인 피보나치 수열을 마주쳤다.

1, 1, 2, 3, 5, 8, 13... 앞의 두 수를 더해서 다음 수를 만드는 간단한 규칙이다.

"재귀 함수 배웠으니까 이걸로 구현하면 멋지겠는데?"

JavaScript

// ❌ 크리스의 순진한 재귀 함수 function fib(n) { if (n <= 2) return 1; return fib(n - 1) + fib(n - 2); } // fib(10) 정도는 금방 나온다. // 하지만 fib(50)을 넣는 순간... 브라우저가 멈췄다.

크리스는 당황했다. 고작 50번째 숫자를 구하는 건데 최신형 맥북이 비명을 지르며 멈춰버린 것이다.

이유는 **"기억력"**이 없기 때문이다. 크리스의 코드는 이미 계산했던 값을 까먹고 또 계산하고, 또 계산하고 있었다.

오늘은 멍청한 반복 계산을 천재적인 효율로 바꿔주는 마법, **동적 프로그래밍(Dynamic Programming, DP)**에 대해 알아본다.

1. 재귀의 저주: 했던 거 또 하기

fib(5)를 구하는 과정을 상상해 보자.

  • fib(5)fib(4)fib(3)을 부른다.
  • fib(4)fib(3)fib(2)를 부른다.
  • fib(3)fib(2)fib(1)을 부른다.
  • ...
  • 그림을 보면 fib(3)이 여러 번 호출되고, fib(2)는 더 많이 호출된다.

    숫자가 조금만 커져도 연산 횟수는 $2^N$으로 폭발한다(지수 시간). fib(50)을 계산하려면 약 1,000조 번 이상의 연산이 필요하다. 컴퓨터가 멈출 만하다.

    2. 동적 프로그래밍의 핵심: 메모이제이션 (Memoization)

    동적 프로그래밍(DP)이라는 이름은 사실 좀 어렵게 들리지만, 개념은 아주 단순하다.

    "한 번 푼 문제는 답을 적어두고(Memo), 나중에 베껴 쓰자!"

    이것을 **메모이제이션(Memoization)**이라고 한다.

    크리스의 코드 심폐소생술

    JavaScript

    // ✅ DP 적용 (Top-Down 방식) function fibMemo(n, memo = []) { // 1. 메모장에 답이 있으면? 계산 안 하고 리턴! (핵심) if (memo[n] !== undefined) return memo[n]; // 2. 기본 조건 if (n <= 2) return 1; // 3. 계산 결과를 메모장에 적어둔다. const res = fibMemo(n - 1, memo) + fibMemo(n - 2, memo); memo[n] = res; return res; }

    이제 fib(50)을 실행해도 0.001초도 안 걸린다.

    이미 계산한 fib(3)을 다시 만나면, 복잡한 트리를 내려가지 않고 배열(memo)에서 값을 쏙 꺼내오기 때문이다($O(1)$).

    전체 시간 복잡도는 **$O(N)$**으로, 1,000조 번의 연산이 단 50번으로 줄어들었다.

    3. 또 다른 방법: 타뷸레이션 (Tabulation)

    메모이제이션이 "위에서 아래로(Top-Down)" 필요한 걸 찾으러 내려가는 방식이라면,

  • *타뷸레이션(Tabulation)**은 "아래서부터 위로(Bottom-Up)" 차근차근 표를 채워 올라가는 방식이다. 재귀를 쓰지 않고 반복문을 쓴다.
  • JavaScript

    // ✅ DP 적용 (Bottom-Up 방식) function fibTable(n) { if (n <= 2) return 1; // 처음부터 표를 만들고 시작한다. const fibNums = [0, 1, 1]; // 3번부터 n번까지 순서대로 채운다. for (let i = 3; i <= n; i++) { fibNums[i] = fibNums[i - 1] + fibNums[i - 2]; } return fibNums[n]; }

    재귀 호출로 인한 스택 오버플로우(Stack Overflow) 위험이 없기 때문에, 데이터가 엄청나게 클 때는 이 방식이 더 안전하다.

    4. 언제 DP를 써야 할까?

    모든 문제에 DP를 쓸 수 있는 건 아니다. 두 가지 조건이 맞아야 한다.

  • 최적 부분 구조 (Optimal Substructure): 큰 문제의 정답을 작은 문제들의 정답을 합쳐서 구할 수 있어야 한다. (예: fib(5) = fib(4) + fib(3))
  • 중복되는 부분 문제 (Overlapping Subproblems): 작은 문제들이 반복해서 등장해야 한다. (예: fib(3)이 계속 필요함)
  • 이 두 가지 냄새가 난다면, 즉시 메모장을 꺼내야 한다.

    5. 결론: 똑똑한 게으름

    동적 프로그래밍은 개발자의 미덕인 **"게으름"**을 알고리즘화한 것이다.

    "아까 계산했는데 왜 또 해? 적어놓고 베껴야지."

    이 단순한 아이디어가 $O(2^N)$이라는 최악의 성능을 $O(N)$이라는 최고의 성능으로 바꿔놓았다.

    알고리즘 대회나 코딩 테스트에서 "경우의 수"나 "최대/최소 비용"을 구하라는 문제의 90%는 바로 이 DP로 푼다.

    그렇다면 실전에서 어떤 문제들을 만났을 때 "아, 이건 DP다!"라고 눈치챌 수 있을까?

    다음 글, "[Algorithm] 동적 프로그래밍을 사용해야 할 때: 실전 패턴 분석" 편에서 계속된다.


    🔗 참고 링크

  • VisualAlgo - Recursion Tree & DP
  • Memoization vs Tabulation
  • 댓글 (0)

    0/1000
    댓글을 불러오는 중...