ABOUT ME

Today
Yesterday
Total
  • Dynamic Programming (with Fibonacci sequence(using Swift and Cache))
    DataStructure + Algorithm/Basic Theories 2021. 10. 19. 17:44

    안녕하세요, 오늘은 Dynamic Programming 에 대해서 다루고자 합니다. Dynamic Programming 에서는  Divide & Conquer 과 같이 덩어리를 조각조각 내어 해를 구합니다. 하지만 둘 사이 가장 다른 점은 그 조각조각의 연관성이라 할 수 있겠습니다. 나눈 것들이 서로 관련이 전혀 없을 때 Divide & Conquer 을, 관련이 있어서 이전에 구한 값을 활용하면서 해를 구할 때 Dynamic Programming 을 활용한다고 할 수 있겠습니다. 나눈 부분들이 서로 관련이 있는데도 Divide & Conquer 방식으로 해결하고자 하면 Dynamic Programming 방식보다 더 비효율적일 가능성이 매우 높겠죠 ? 이번 글에서는 Fibonacci Sequence 를 여러가지 방식으로 해결하면서 이번 주제를 더 상세히 설명하고자 합니다. 

     

     

     

     

     Fibonacci Sequence

    Dynamic Programming 을 다루기 전에, Fibonacci Sequence 는 n 번째 수가 n-1, n-2 번째 수의 합이 되는 수를 나열한 것입니다. 

     

    수식으로 하면 아래와 같습니다. 

    f(n) = f(n-1) + f(n-2)      (for n >= 2, n : Int ) 
    f(n) = n ( for n == 0 || n == 1 ) 
    0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, ... 

     

     Divide & Conquer 방식을 이용할 때 다음과 같이해결할 수 있습니다. 

    func fibonacciRecursion(_ n: Int) -> Int {
        return n < 2 ? n : fibonacciRecursion(n-1) + fibonacciRecursion(n-2) 
    }

    생각보다 귀엽고 간단하죠? 하지만 Time Complexity 는 그리 귀엽지 않습니다. 너무 너무 느려요. 

     

    예를 들어 10 번째 element (f(10))  를 구할 때는  f(9), f(8) 이 필요합니다.

    f(9) 를 구할 때 f(8) 과 f(7) 이 필요한데, 이 때에 이전에 구했던 값을 이용하지 않고 다시 구하기 때문에 불필요한 과정을 많이 포함하게 되는거죠. 

     

    실제로 TimeComplexity 를 구해보면 O(2^n ) 임을 알 수 있어요. 

    2 -> 1 + 0   2번

    3 -> 2 ( 1+0 ) + 1  4번

    4 -> 4 ( 2+ 3 ) + 3 -> 1 + 2 + 4 + 4 11 번 
    5 -> 4 + 3 -> 15번  

     

    Divide And Conquer 은 쉽게 말하면 작게 나누어서 푸는 방법을 말합니다. 
    작게 나눈 부분부분이 연관성이 있어도 반복해서 계산하기 때문에, 이 경우 매우 비효율적입니다. ( 서로 연관 없는 곳에 쓰세요..)

     

     

     

    이런 경우, 문제를 어떻게 해결하면 좋을까요 ?? 반복되는 계산을 미리 다른 곳에 저장하거나 다른 방법으로 이용해서 더 빠르게 할 수는 없을까요 ?? 

     

     

     

     

     

    Dynamic Programming

    위 Fibonacci Sequence 는 Divide & Conquer 방식의 Recursion 을 사용하여 풀 수 있으나, 그렇게 하는 경우 O( 2^n ) 이 나오게 되었죠?. 반복되는 값들을 이용하지 않고 계속해서 새로 계산해주었기 때문에 비효율적이라는 것을 알 수 있었습니다. 하지만, Dynamic Programming 을 이용해 푸는 경우 O(n) 까지 빨라질 수 있습니다. Dynamic Programming 에서는 서로 연관된, 반복되는 instance 를 활용하기 때문에, 방식의 차이가 Time Complexity 의 차이를 낳았습니다. 

     

    위에서 잠깐 언급했지만, Dynamic Programming 은 Divide & Conquer 방식과 사용하는 경우가 다릅니다. Divide & Conquer 의 경우 나눈 후 나온 데이터가 서로 연관이 없지만, Dynamic Programming 의 경우 그렇지 않습니다. 즉,  나눈 부분부분이 서로 연관성이 있고 활용할 수 있을 때 Dynamic Programming 을 활용하고, 그렇지 않은 경우 Divide & Conquer 방식을 사용하면 좋습니다. 따라서, Divide & Conquer 은 top to bottom 으로 비유되는 데 반해  Dynamic Programming 은 bottom to top 으로 볼 수 있습니다. n+1 번째 element 를 구하기 위해 이미 구했던 n, n-1 번째 element 를 활용하기 때문이죠. 

     

     

     

     

     

     

    이제 Dynamic Programming 을 이용하여 더 빠르고 효율적으로 풀어보겠습니다.

     

    이번 예제에서는 'Memoization' 을 활용합니다. 

     

    Cache  Memory 를 사용하는 방식인데, 해당 방식을 사용할 경우 O(n) 으로 줄일 수 있습니다. 

     

    memoization 은 주로 컴퓨터 프로그램의 속도를 향상시킬 목적으로 쓰이는 최적화 테크닉을 말합니다.
    부담되는 function 이 내놓은 결과값을 저장하고(cache) 같은 값이 필요할 때 돌려주는 방식입니다.  
    더보기

    ( Memoization 과 Memorization 생긴게 많이 비슷하죠 ? 둘다 'to remember' 를 뜻하는  라틴어 'memoro' 에서 왔대요)

     

    방식은 다음과 같아요

    var fibonacciCache = [Int: Int]()
    
    func fibWithMemo(_ number: Int) -> Int {
    
        if let value = fibonacciCache[number] {
            return value
        }
    
        let newValue = number < 2 ? number : fibWithMemo(number - 1) + fibWithMemo(number - 2)
        fibonacciCache[number] = newValue
        return newValue
    }

    먼저, global variable 로 fibonacciCache 를 저장합니다.  그 후, fibonacci(_:) function 이 호출 되었을 때 해당 값을 바꿔주면서 작업을 빠른속도로 할 수 있습니다. 

     

    실제 fibonacci function 의 parameter 로 값을 다르게 넣어주면서 함수 호출횟수를 조회해보면, 아래와 같습니다. 

    parameter fibonacci funtion 이 호출된 수
    1 1
    2 3
    3 5
    4 7
    5 9
    n 2(n-1)

    이때, 함수 호출이 끝나고 나서는 global variable fibonacciCache 의 값이 변화된 상태입니다. 

     

     

     

     

     

     

    추가로 Cache 를 이용한 fibonacci function 을 몇개 더 보도록 할게요! (Cache 는 헷갈리고 중요하니까..)

     

    func fibWithMemo2(_ n: Int) -> Int {
        
        var fibonacciCache2 = [Int: Int]()
    
        func fibonacci(_ number: Int) -> Int {
            if let value = fibonacciCache2[number] {
                return value
            }
    
            let newValue = number < 2 ? number : fibonacci(number - 1) + fibonacci(number - 2)
            fibonacciCache2[number] = newValue
            return newValue
        }
    
        fibonacci(n)
        return fibonacciCache2[n]!
    }

    해당 fibWithMemo2 는, global variable 을 사용하지 않고, function 내에서 Cache 를 선언한 후 내부에서 빠르게 처리합니다. 

     

    global variable 을 선언하지 않기 때문에 만약 해당 기능이 이 매우 빈번하게 사용되지는 않는다고 한다면 이 함수를 더 사용할 것 같네요. 

    (경우에 따라 당연히 다른 방법을 사용하는게 맞겠죠?? )

     

     

     

    func fibWithMemo3(_ n: Int) -> Int {
        if n <= 1 { return n}
    
        var fibonacciCache3 : [Int : Int] = [0:0, 1:1]
    
        func realFib(_ input: Int) { // calculate the result of fibonacci sequence
            fibonacciCache3[input] = fibonacciCache3[input - 1]! + fibonacciCache3[input - 2]!
        }
    
        for index in 2 ... n {
            realFib(index)
        }
    
        return fibonacciCache3[n]!
    }

    이번 함수는, fibWithMemo2 와 전반적으로 같고 fibonacci sequence 를 구하는 방법만 약간 다릅니다. Recursion 을 사용하지 않고 차례대로 for 문을 이용해서 구해요. 

     

     

     

    Dynamic Programming 에서는 작은 instances 를 먼저 해결하고 결과를 저장한 후, 그 값이 필요할 때면 (다시 계산하는 대신) 언제든 활용합니다. 여기서 "programming" 은 solution 들이 형성되는 array (table) 의 사용을 의미합니다. Dynamic programming algorithm 에서는 array 내에서 바닥부터 (0번째 element) solution 을 형성해 나갑니다. (bottom-up approach) 

     

    dynamic programming algorithm 개발에서의 순서는 다음과 같습니다.

    1. Establish a recursive property that gives the solution to an instance of the problem.

    2. Bottom-up 방식, 더 작은 문제들을 먼저 해결하는 방식으로 진행합니다. 

     

     

    마지막으로 정리를 하자면, Dynamic Programming 은 Divide & Conquer 방식과 사용하는 경우가 다릅니다. Divide & Conquer 의 경우 나눈 후 나온 데이터가 서로 연관이 없지만, Dynamic Programming 의 경우 그렇지 않아요. 다시 말하면, Dynamic Programming 에서는 나눈 부분부분이 서로 연관성이 있고 활용할 수 있습니다. (이 글에서는 활용을 위해 데이터를 Cache 에 잠시 저장해서 사용하였습니다.) 따라서, Divide & Conquer 은 top to bottom 으로 비유되는 데 반해  Dynamic Programming 은 bottom to top 으로 볼 수 있습니다. (Divide & Conquer 의 대표적인 예시로는 Merge Sort 가 있지요)  아래 마지막 fibonacci function 을 끝으로 글을 마치겠습니다. 읽어주셔서 감사합니다. 

     

     

     

     

     

     

     

     

     

    func fibonacciWithArray(_ n: Int) -> Int {
        var array = [1,1]
        
        if n > 1 {
            while (array.count <= n) {
                count += 1
                array.append(array[array.count-1] + array[array.count-2])
            }
    
            return array[array.count - 1]
        } else {
            return array[n]
        }
    }

    사실.. fibonacci 의 경우 Recursion 대신 Array 를 통해 풀면 Cache 의 도움을 받지 않고도 아주 빠르게 해결할 수 있습니다.

    (해당 글은 Dynamic Programming, Cache 를 알아보기 위해 Fibonacci 를 이용했지만..  ) 

     

     

     



    출처:  Richard Neapolitan. “Foundations of Algorithms." (5th) 에서 일부를 발췌했습니다. 

    'DataStructure + Algorithm > Basic Theories' 카테고리의 다른 글

    Quick Sort with Swift  (0) 2021.10.21
    MergeSort (with Swift)  (0) 2021.10.20
    LinkedList  (0) 2021.10.13
    Stack, Queue With Swift  (0) 2021.10.12
    Insertion Sort with Swift  (0) 2021.10.09
Designed by Tistory.