Fibonacci Number
피보나치 수는 F(0) = 0과 F(1) = 1로 시작하며, 그 이후의 각 수는 앞의 두 수의 합입니다: F(n) = F(n-1) + F(n-2). 수열은 0, 1, 1, 2, 3, 5, 8, 13으로 시작합니다. 함수는 n을 받아 F(n)을 반환합니다.
함수
- ninteger
- 0부터 세는 피보나치 수열에서의 위치
- 반환값integer
- 피보나치 수 F(n)
제약 조건
0 ≤ n ≤ 45- 답은 부호 있는 32비트 정수에 들어갑니다:
F(45) = 1134903170.
예제
- 입력
- n = 4
- 출력
- 3
- 설명
- 시작부터 세어 올라갑니다:
F(2) = 1 + 0 = 1,F(3) = 1 + 1 = 2, 그리고F(4) = 2 + 1 = 3.
- 입력
- n = 10
- 출력
- 55
- 설명
- 인덱스 0부터 시작하는 수열은 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55입니다. 인덱스 10의 숫자는
34 + 21 = 55입니다.
제출 시 숨은 테스트 +13개
후속 질문
O(log n) 시간 안에 F(n)을 계산할 수 있나요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
재귀적 정의를 사용해 손으로
F(5)를 계산해 보세요. 어떤 값들을 두 번 이상 계산하게 되나요?각 피보나치 수를 구하는 데는 그 앞의 두 수만 필요합니다. 오름차순으로 계산하면 필요한 모든 값을 필요할 때 이미 알고 있습니다.
0과1에서 시작하세요.n-1번 반복하세요. 가지고 있는 두 수를 더한 다음, 더 오래된 수를 버리고 합을 유지하세요.
풀이
정의 자체가 이미 재귀 함수이며, 이를 그대로 작성하면 올바른 답을 얻습니다. 문제는 실행 시간입니다. 두 재귀 호출이 서로의 작업을 다시 수행하고, 호출 횟수는 n에 따라 기하급수적으로 증가합니다. 동적 프로그래밍은 각 피보나치 수를 아래에서 위로 한 번씩만 계산하여 이 문제를 해결합니다. 마지막 단계에서는 다음 수를 계산하는 데 필요한 두 수만 유지합니다.
정의에서 바로 나온 재귀
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
정의를 한 글자도 빠짐없이 그대로 옮기세요. fib(0)은 0이고, fib(1)은 1이며, 그보다 큰 값은 모두 fib(n-1) + fib(n-2)를 반환합니다. 모든 호출 체인은 두 기본 사례 중 하나에서 끝나므로 답은 정확합니다.
이제 호출 횟수를 세어 보세요. fib(5)는 fib(4)와 fib(3)을 호출하지만, fib(4)도 fib(3)을 다시 호출합니다. 결국 fib(3)은 두 번, fib(2)는 세 번, fib(1)은 다섯 번 실행되고, fib(5)는 총 15번 호출합니다. 같은 값들이 계속해서 반복 계산됩니다.
호출 횟수는 피보나치 수 자체를 따릅니다. F(n)을 계산하면 2 × F(n+1) - 1번 호출합니다. n = 45일 때 이는 약 3.7 × 10^9번의 호출로, 시간 제한을 훨씬 초과합니다. 상한은 보통 O(2^n)으로 표기하며, 정확한 증가율은 약 1.618^n입니다. 재귀 깊이는 n단계에 불과하므로 스택에는 O(n)의 공간이 필요합니다.
알고리즘
n이0또는1이면n을 반환합니다.- 그렇지 않으면
n-1과n-2에 대해 함수를 호출합니다. - 두 결과의 합을 반환합니다.
def fib(n):
if n < 2:
return n # F(0) = 0, F(1) = 1
return fib(n - 1) + fib(n - 2)표를 아래에서 위로 채우기
핵심 아이디어
재귀가 느린 것은 값을 기억하지 않기 때문입니다. 각 피보나치 수를 처음 계산할 때 적어 두면, 각 수를 계산하는 데 덧셈 한 번이면 됩니다. 인덱스 0부터 n까지의 칸이 있는 표 f를 만들고, f[0] = 0과 f[1] = 1을 설정한 다음, 나머지를 왼쪽에서 오른쪽으로 f[i] = f[i-1] + f[i-2]를 이용해 채우세요.
왼쪽에서 오른쪽으로 채우는 순서가 이 방법을 가능하게 합니다. f[i]에 도달했을 때 필요한 두 수가 이미 표에 들어 있기 때문입니다. n = 10일 때 표는 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 순으로 채워지고, 답은 마지막 칸에 있습니다.
이것은 가장 기본적인 형태의 동적 프로그래밍으로, 점화식과 더 작은 경우들의 답을 담은 표로 이루어집니다. 덧셈은 n-1번 필요하며, 시간 복잡도는 O(n)입니다. 표에는 n + 1개의 수가 들어가므로 공간 복잡도는 O(n)입니다. 이제 n = 45를 계산하는 데는 수십억 번의 호출 대신 덧셈 44번이면 됩니다.
알고리즘
n이0또는1이면n을 반환합니다.f[0] = 0과f[1] = 1로n + 1개의 숫자로 이루어진 표를 만듭니다.- 2부터
n까지의i에 대해f[i] = f[i-1] + f[i-2]로 설정합니다. f[n]을 반환합니다.
def fib(n):
if n < 2:
return n
f = [0] * (n + 1) # f[i] will hold F(i)
f[1] = 1
for i in range(2, n + 1):
f[i] = f[i - 1] + f[i - 2]
return f[n]마지막 두 숫자만 남기기
핵심 아이디어
테이블 반복문이 무엇을 읽는지 살펴보세요. f[i]를 채우려면 f[i-1]과 f[i-2]만 필요하고 그보다 오래된 값은 필요하지 않으므로, 이전의 모든 항목은 불필요합니다. 테이블 대신 변수 두 개를 사용하세요. prev는 두 단계 전의 숫자를 저장하고 curr는 한 단계 전의 숫자를 저장합니다.
F(0)과 F(1)에 해당하는 prev = 0과 curr = 1로 시작하세요. 각 단계에서 next = prev + curr를 계산한 다음, 두 값을 앞으로 한 칸씩 옮깁니다. prev에는 이전의 curr을 넣고, curr에는 next를 넣습니다. n = 4일 때 쌍은 (0, 1)에서 (1, 1), (1, 2), (2, 3)으로 이동하며, curr = 3이 답입니다.
연산은 동일하게 n-1번 덧셈을 하므로 시간 복잡도는 O(n)이고, 메모리에는 정수 세 개만 사용하므로 공간 복잡도는 O(1)입니다. 값을 갱신하는 순서가 중요합니다. 더하기 전에 prev를 덮어쓰면 잘못된 값으로 합을 계산하게 됩니다.
알고리즘
n이0또는1이면n을 반환합니다.prev = 0과curr = 1을 설정합니다.n-1회 반복합니다:next = prev + curr를 계산한 다음prev = curr와curr = next를 설정합니다.curr를 반환합니다.
def fib(n):
if n < 2:
return n
prev, curr = 0, 1 # F(0) and F(1)
for _ in range(n - 1):
prev, curr = curr, prev + curr
return curr
함정과 경계 사례
피보나치는 동적 프로그래밍의 대표적인 첫 문제이며, 대부분의 버그는 재귀나 처음 두 값에서 비롯됩니다.
- 단순 재귀를 제출하는 경우입니다. 작은 테스트는 통과하지만
n = 45에서는 호출이 수십억 번 필요합니다. 결과를 테이블이나 두 개의 변수에 저장하세요. - 시작 값을 잘못 설정하는 경우입니다. 여기서는
F(0) = 0이고F(1) = 1이므로F(2) = 1,F(10) = 55입니다. 수열을 1, 1에서 시작하면 모든 답의 인덱스가 하나씩 밀립니다. - 작은
n에 대한 예외 처리를 하지 않고 테이블을 만드는 경우입니다.n = 0일 때 크기가n + 1 = 1인 테이블에는f[1]을 저장할 자리가 없으며, 여기에 값을 쓰면 범위를 벗어납니다.n < 2이면 즉시n을 반환하세요. - 쌍의 값을 잘못된 순서로 갱신하는 경우입니다.
prev = curr다음에curr = prev + curr를 실행하면 새prev를 더해curr가 두 배가 됩니다. 먼저 합계를next에 계산하거나, 언어에서 지원한다면 동시 대입을 사용하세요. - 한 단계 더 실행하는 경우입니다.
F(n+1)도 계산하는 루프는 제한에 도달했을 때F(46) = 1836311903을 계산하는데, 이는 운 좋게도 32비트에 들어갑니다.F(47)은 들어가지 않습니다.
자주 묻는 질문4
재귀 피보나치 함수의 시간 복잡도는 얼마인가요?
단순 재귀는 2 × F(n+1) - 1번 호출하며, 이 횟수는 1.618^n처럼 증가하고 보통 O(2^n)으로 표기합니다. n = 45일 때 호출 횟수는 약 3.7 × 10^9회입니다. 각 결과를 표나 두 개의 변수에 한 번씩 저장하면 O(n)으로 줄어듭니다.
동적 프로그래밍으로 피보나치 수열을 어떻게 풀까요?
점화식 F(n) = F(n-1) + F(n-2)에서 시작해 n이 증가하는 순서대로 값을 계산하고 각각 저장하세요. 아래에서 위로 표를 채우거나 재귀 함수를 유지하면서 결과를 캐시할 수 있는데, 이를 메모이제이션이라고 합니다. 어느 쪽이든 각 값은 한 번씩만 계산되므로 전체 작업량은 O(n)입니다.
피보나치를 O(1) 공간에서 계산할 수 있을까요?
네. 각 숫자는 바로 앞의 두 숫자에만 의존하므로 변수 두 개면 충분합니다. 마지막 두 값을 유지하고 매 단계마다 앞으로 이동시키세요. 그러면 O(n) 시간에 O(1)의 추가 공간을 사용합니다.
O(n)보다 더 빠른 방법이 있나요?
네. 행렬 [[1, 1], [1, 0]]을 n제곱하면 오른쪽 위 모서리에 F(n)이 들어가며, 반복 제곱을 사용하면 행렬 곱셈 O(log n)번으로 그 거듭제곱을 계산할 수 있습니다. 황금비의 거듭제곱을 사용한 닫힌 형태의 공식도 있지만, 부동 소수점 연산을 사용해 n이 커질수록 정밀도가 떨어지므로 정수 방식이 더 적합합니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def fib(n):
# 여기에 코드를 작성하세요케이스 1
케이스 2
입력
n = 4
기대값
3