다양한 피보나치 수열 알고리즘
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) = 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
이렇게 하면 시간을 초과하지 않고 통과를 함
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(2^{n})$으로 상당히 시간복잡도가 큼
-----------------------------------------------------------------------------------------------------------------------
두번째 방법은 위와 같이 반복문을 이용한 방법
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개를 이용해서 구하고…
이것이 반복된단 말이지
근데 이전의 수들이 중복된다는게 문제임
예를 들어 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]로 구해서 저장을 해놓는다
---------------------------------------------------------------------------------------------------------------------
다음으로 일반항을 이용해서도 풀 수 있다고함
점화식 $$a_{n+2} = pa_{n+1} + qa_{n}$$을 특성방정식 $$x^{2} = px + q$$으로 바꿔서
이 방정식의 근이 $\alpha, \beta$이면 점화식의 해는 $$a_{n} = C_{1}\alpha^{n} + C_{2}\beta^{n}$$
초기 조건 $a_{0}, a_{1}$에 대하여 계수 $C_{1}, C_{2}$를 구할 수 있다는 사실을 이용한다.
그러면 피보나치수열의 일반항은 $a_{1} = 1, a_{2} = 1$일 때 \[a_{n} = \frac{1}{\sqrt{5}}((\frac{1+\sqrt{5}}{2})^{n}-(\frac{1-\sqrt{5}}{2})^{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번째 피보나치 수를 $F_{n}$이라고 한다면 피보나치 수를 다음과 같이 행렬화할 수 있다고 한다.
\[\begin{pmatrix}F_{n+1} & F_{n} \\F_{n} & F_{n-1} \\\end{pmatrix}=\begin{pmatrix}1 & 1 \\1 & 0 \\\end{pmatrix}^{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를 계속해서 곱하면 구할 수 있지만
더욱 효율적인 방법은 $2^1$을 구한다음에 이 결과를 제곱하여 $2^2$을 구하고
$2^2$을 또 제곱하여 $2^4$을 구하고… $2^4$을 또 제곱하여 $2^8$을 구하고…
2의 지수가 2의 제곱이 아니면 문제일 수 있지만
모든 자연수는 2의 제곱수의 합으로 표현할 수 있다
예를 들어 $2^{100}$을 구하기 위해서 100을 2의 제곱수의 합으로 표현할 수 있는데
100은 4와 32와 64의 합이다.
그래서 $2^{100}$은 $2^4$, $2^{32}$, $2^{64}$를 곱하면 된다
이러면 $O(log_{2} n)$으로 아주 빠르게 구할 수 있다고 한다
6. 되돌아보기
동적계획법이라는거 몰랐는데… 그냥 풀어버리네
그동안 공부한게 효과있다는 말이겠지.?
어떻게 하면 시간을 줄일 수 있을까?? 생각을 하면서 코딩을 하자
재귀함수는 시간복잡도를 상당히 늘린다
한번 구한 값을 어딘가에 저장해서 필요할때마다 불러 쓸 수 있다면??
리스트 같은데 저장해서 불러 쓰는건 사실 알게모르게 많이했단 말이지
예를 들면 무슨 시험볼때 factorial을 리스트에 미리 저장해놔서 필요할때 index로 불러온적 있었지… 그거랑 똑같아
7. 참고
'알고리즘 > 알고리즘 일반' 카테고리의 다른 글
선형구조와 비선형구조 (0) | 2022.01.02 |
---|---|
논리적으로 차분하게 코딩하기 (0) | 2022.01.02 |
반복문에서 경우의 수를 나누는 방법 (0) | 2021.11.28 |
stack 필수 활용 기술 3 (0) | 2021.11.27 |
재귀함수 활용하기 (0) | 2021.11.26 |