Processing math: 100%
 

다양한 피보나치 수열 알고리즘

1. 문제

 

https://programmers.co.kr/learn/courses/30/lessons/12945

 

코딩테스트 연습 - 피보나치 수

피보나치 수는 F(0) = 0, F(1) = 1일 때, 1 이상의 n에 대하여 F(n) = F(n-1) + F(n-2) 가 적용되는 수 입니다. 예를들어 F(2) = F(0) + F(1) = 0 + 1 = 1 F(3) = F(1) + F(2) = 1 + 1 = 2 F(4) = F(2) + F(3) = 1 + 2 = 3 F(5) = F(3) + F(4) =

programmers.co.kr

 

피보나치 수는 F(0)=0,F(1)=1일 때 1 이상의 n에 대하여 F(n)=F(n1)+F(n2)가 적용되는 수 입니다.

 

예를 들어

 

F(2)=F(0)+F(1)=0+1=1

 

F(3)=F(1)+F(2)=1+1=2

 

F(4)=F(2)+F(3)=1+2=3

 

F(5)=F(3)+F(4)=2+3=5와 같이 이어집니다.

 

2 이상의 n이 입력되었을 때, n번째 피보나치 수를 1234567로 나눈 나머지를 리턴하는 함수를 완성하세요

 

 

2. 제한사항

 

n은 1이상, 100000이하인 자연수

 

 

3. 예시

 

n=3이면 return=2

 

n=5이면 return=5

 

피보나치 수는 0번째부터 0,1,1,2,3,5,...와 같이 이어진다

 

 

4. 나의 풀이

 

피보나치 수 하면 재귀적 알고리즘이니까…

 

def solution(n):
    
    answer = 0
    
    def fib(n):
        
        if n == 0:
            
            return 0
        
        elif n == 1:
            
            return 1
        
        else:
        
            return fib(n-1)+fib(n-2)
            
    return (fib(n) % 1234567)

 

이러면 시간초과된다..

 

n=40이면 fib(40)도 10초안에 계산을 못함

 

재귀적 알고리즘이 시간을 많이 소요한다고 했던 것 같아서

 

내가 생각할 수 있는 방법은

 

리스트에 원소를 두개씩 빼서 더해가지고 리스트에 추가하는 방식으로..

 

zero index로 피보나치 수를 세고 있으니까

 

def solution(n):
    
    answer = 0
    
    fib_list = [0,1]
    
    m = 2
    
    while m != n+1:
        
        fib_list.append(fib_list[m-2] + fib_list[m-1])
        
        m = m + 1
        
    answer = (fib_list[n] % 1234567)
    
    return answer

 

fib_list=[0,1]로 초기 리스트를 만든다

 

2번째 피보나치 수부터 구해야하니까 m=2라고 함

 

def solution(n):
    
    answer = 0
    
    fib_list = [0,1]
    
    m = 2

 

반복문을 통해서 m이 n+1이 될때까지 반복을 할건데

 

m이 n+1이 되면 반복문을 탈출을 할 것..

 

m != n일때 종료를 하면 m=n이 되는 순간에 n번째 원소를 구하지 못하고 반복문을 탈출하니까

 

m != n+1이라고 해가지고 m=n이 되더라도 종료하지 않고 n번째 원소를 구할 수 있도록 한다

 

그러니까 n번째 피보나치 수열의 값을 구할 때까지 반복을 하게 됨

 

m번째 원소 바로 이전의 두 원소를 더해서 fib_list에 추가시킨다

 

한번 반복하면 m에 1을 더함

 

while m != n+1:
        
    fib_list.append(fib_list[m-2] + fib_list[m-1])

    m = m + 1

answer = (fib_list[n] % 1234567)

return answer

 

이렇게 하면 시간을 초과하지 않고 통과를 함

 

제목 없음.jpg

 

 

5. 다른 풀이

 

피보나치 수열만 구할 수 있으면 되니까 피보나치 수열을 구하는 여러가지 알고리즘을 조사해봄

 

내가 했던 첫번째 재귀적인 풀이는

 

def solution(n):
    
    answer = 0
    
    def fib(n):
        
        if n == 0:
            
            return 0
        
        elif n == 1:
            
            return 1
        
        else:
        
            return fib(n-1)+fib(n-2)
            
    return (fib(n) % 1234567)

 

O(2n)으로 상당히 시간복잡도가 큼

 

-----------------------------------------------------------------------------------------------------------------------

 

두번째 방법은 위와 같이 반복문을 이용한 방법

 

def solution(n):
    
    if n < 2:
        
        return n
        
    else:
        
        a,b = 0,1
        
        for i in range(n):
            
            a,b = b,a+b
            
        return (a%1234567)

먼저 n이 2보다 작으면… 즉 n이 0과 1이면 0번째 값은 0이고 1번째 값은 1이니까 바로 n을 return하도록 하고

 

n이 2이상인 경우에는

 

초기 원소 a=0,b=1로 주면 각각 0번째, 1번째 수이고

 

반복문을 n번 도는데

 

n=1번 돌면 a=1, b=1+0=1이 되니까 각각 1번째 2번째 수가 된다

 

n=2번 돌면 a=1, b=1+1=2가 되니까 각각 2번째 3번째 수가 된다

 

n=3번 돌면 a=2, b=1+2=3이 되니까 각각 3번째 4번째 수가 된다

 

n=4번 돌면 a=3, b=2+3=5이 되니까 각각 4번째 5번째 수가 된다

 

결국 n번 돌면 a에 n번째 피보나치 수가 들어가고 이것을 1234567로 나눈 나머지를 return

 

반복문을 n번 도는게 끝이라 시간복잡도는 O(n)으로 상당히 효율적인 알고리즘

-----------------------------------------------------------------------------------------------------------------------

 

내가 푼 방법이 (의도하진 않았지만) 동적계획법을 이용한 방법인데..

 

시간복잡도가 O(n)정도라고 함.. 근데 두번째 방법이 실제로 시간이 더 빠른듯

 

설명을 읽어보면 핵심이 피보나치 수열을 구하기 위해서 이전 피보나치 수를 이용해서 구해야하는데…

 

이전 피보나치 수 2개는 또 이전 피보나치 수 4개를 이용해서 구하고…

 

이것이 반복된단 말이지

 

근데 이전의 수들이 중복된다는게 문제임

 

etc-image-1
그림1. 7번째 피보나치 수열의 수를 구하는 흐름

 

예를 들어 fib(7)을 구하기 위해서 fib(3)을 몇개나 사용했는지 세보면 5개임

 

만약 재귀적 풀이를 이용했다면 fib(3)이라는 함수를 5번이나 더 호출해야한다는 거

 

이렇게 중복된다는 것이 큰 문제인데

 

그렇다면 이전 문제의 값을 1번만 구하고 어딘가에 저장한

 

다음에 필요할때마다 쓰겠다는 것이 동적계획법의 핵심적인 아이디어다

 

fib(3)을 단 1번만 호출하고 어딘가에 저장해놓은 다음에 나중에 필요할때마다 쓰겠다 이거임

 

def solution(n):

    if n <2:
    
        return n
    
    fib = [0 for _ in range(n+1)]
    
    fib[1] = 1
    
    for i in range(2,n+1):
        
        fib[i] = fib[i-1] + fib[i-2]
    
    return fib[n]%1234567

 

fib라는 dynamic table을 미리 0을 채워넣어서 만들어놓고

 

fib[0]=0이고 fib[1]=1이니까 초기 설정을 한 뒤에

 

i=2부터 n까지 반복문을 돌아

 

fib[i]를 fib[i-1]+fib[i-2]로 구해서 저장을 해놓는다

 

---------------------------------------------------------------------------------------------------------------------

다음으로 일반항을 이용해서도 풀 수 있다고함

 

점화식 an+2=pan+1+qan을 특성방정식 x2=px+q으로 바꿔서

 

이 방정식의 근이 α,β이면 점화식의 해는 an=C1αn+C2βn

 

초기 조건 a0,a1에 대하여 계수 C1,C2를 구할 수 있다는 사실을 이용한다.

 

그러면 피보나치수열의 일반항은 a1=1,a2=1일 때 an=15((1+52)n(152)n)

 

def fibo(n):
    
    sqrt_5 = 5 ** (1/2)
    
    ans = 1/sqrt_5 * (((1+sqrt_5)/2)**n - ((1-sqrt_5)/2)**n)
    
    return int(ans)

 

시간복잡도가 O(logn)정도로 간단하면서도 가장 빠르다

 

------------------------------------------------------------------------------------------------------------------------------

 

마지막으로 행렬의 곱셈을 이용해서도 풀 수 있다고 한다

 

만약 n번째 피보나치 수를 Fn이라고 한다면 피보나치 수를 다음과 같이 행렬화할 수 있다고 한다.

 

(Fn+1FnFnFn1)=(1110)n

 

 

위 식의 오른쪽 행렬을 n번 제곱해서 (0,1)이나 (1,0)의 원소를 구하면 된다

 

def solution(n):
    
    size = 2
    zero = [[1,0],[0,1]] #항등행렬
    base = [[1,1],[1,0]] #곱셈을 시작할 오른쪽 기본 행렬
    
    #두 정사각행렬의 곱
    def square_matrix_mul(a,b,size=size):
        
        new = [[0 for _ in range(size)] for _ in range(size)]
        
        for i in range(size):
            
            for j in range(size):
                
                for k in range(size):
                    
                    new[i][j] += a[i][k] * b[k][j]
                    
        return new
        
        #행렬의 n제곱
    def get_nth(n):
        
        matrix = zero.copy()
        
        k = 0
        
        tmp = base.copy()
        
        while 2 ** k <= n:
            
            if n & (1 << k) != 0:
                
                matrix = square_matrix_mul(matrix,tmp)
                
            k+= 1
            
            tmp = square_matrix_mul(tmp, tmp)
            
        return matrix
    
    return get_nth(n)[1][0] % 1234567

 

2의 제곱수는 2를 계속해서 곱하면 구할 수 있지만

 

더욱 효율적인 방법은 21을 구한다음에 이 결과를 제곱하여 22을 구하고

 

22을 또 제곱하여 24을 구하고… 24을 또 제곱하여 28을 구하고…

 

2의 지수가 2의 제곱이 아니면 문제일 수 있지만

 

모든 자연수는 2의 제곱수의 합으로 표현할 수 있다

 

예를 들어 2100을 구하기 위해서 100을 2의 제곱수의 합으로 표현할 수 있는데

 

100은 4와 32와 64의 합이다.

 

그래서 210024, 232, 264를 곱하면 된다

 

이러면 O(log2n)으로 아주 빠르게 구할 수 있다고 한다

 

 

6. 되돌아보기

 

동적계획법이라는거 몰랐는데… 그냥 풀어버리네

 

그동안 공부한게 효과있다는 말이겠지.?

 

어떻게 하면 시간을 줄일 수 있을까?? 생각을 하면서 코딩을 하자

 

재귀함수는 시간복잡도를 상당히 늘린다

 

한번 구한 값을 어딘가에 저장해서 필요할때마다 불러 쓸 수 있다면??

 

리스트 같은데 저장해서 불러 쓰는건 사실 알게모르게 많이했단 말이지

 

예를 들면 무슨 시험볼때 factorial을 리스트에 미리 저장해놔서 필요할때 index로 불러온적 있었지… 그거랑 똑같아

 

 

7. 참고

 

https://shoark7.github.io/programming/algorithm/%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%84-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-5%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95

 

피보나치 수열 알고리즘을 해결하는 5가지 방법

Let me introduce 5 different ways to solve fibonacci algorithm

shoark7.github.io

 

 

728x90