포스트

[코테] 피보나치 수 (lv.2)

[코테] 피보나치 수 (lv.2)

1. 문제 핵심 요약

문제 링크: https://school.programmers.co.kr/learn/courses/30/lessons/12945?language=java

핵심 요구사항: N번째 피보나치 수를 1234567로 나눈 값을 리턴

제한 조건: $2 \le N \le 100,000$

2. 접근 방식

0, 1번 배열 값을 적어두면 그 이후로는 f(N) = f(N-1) + f(N-2)로 N번 배열에 저장하고 꺼내오면 된다고 생각했다. 추가로 범위가 10만까지라서 int의 범위로는 불가능할 것 같아 long으로 진행하려고 구상했다.

3. 장단점 및 복잡도 분석

접근 방식장점단점시간 복잡도공간 복잡도
배열 사용구현이 직관적임N이 크면 메모리 낭비가 심함$O(N)$$O(N)$
변수 swap 사용메모리를 절약할 수 있음과거의 값이 남지 않음$O(N)$$O(1)$
재귀와 메모이제이션수학적 점화식을 코드로 표현 가능N이 만 단위로 커지면 SOF 발생할 위험 있음$O(N)$$O(N)$
행렬 분할 정복N이 커도 계산 빠름구현 난이도가 높음, N이 작을 때 느릴 수 있음$O(\log N)$$O(1)$

4. 구현 중 발생한 문제 및 해결

❌ 문제 발생

현상: 7번 테스트 케이스부터 실패 발생했다.

원인 분석: long으로 변경했음에도 피보나치 수열은 기하급수적으로 증가해서 N = 93 이후로는 long의 최대 범위를 초과하기 때문이라고 한다.

⭕ 해결 방법

기존에는 1234567 나누기 연산을 리턴에서 진행하여 배열에 자료형 범위보다 큰 값을 저장하려 해서 오버플로우가 발생했다. 그래서 생각을 바꿔 배열에 피보나치 수를 저장할때부터 1234567로 나눈 나머지를 저장하도록 변경하고 자료형도 int로 변경했다.

5. 최종 소스 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 배열을 사용한 피보나치 수
class Solution {
    public int solution(int N) {
        int[] arr = new int[N+1];
			arr[0] = 0;
			arr[1] = 1;

			for(int i = 2; i <= N; i++) {
				arr[i] = (arr[i - 1] + arr[i - 2]) % 1234567;
			}
            
        return arr[N];
    }
}

6. 다른 방식 구현

행렬 분할 정복 방식을 사용한 피보나치 수열 계산이 궁금해서 직접 해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 행렬 분할 정복 방식을 사용한 피보나치 수
class Solution {
    // 1. 초기 행렬 설정
				// res = 단위 행렬 항등원
				long r00 = 1, r01 = 0;
				long r10 = 0, r11 = 1;

				// base = 피보나치 기본 행렬 [[1, 1], [1, 0]]
				long b00 = 1, b01 = 1;
				long b10 = 1, b11 = 0;

				// N번째 피보나치 수를 구하기 위해 행렬을 (N - 1)번 거듭제곱해야 함
				int exp = N - 1;

				// 2. 분할 정복을 이용한 행렬 거듭제곱 루프 (Matrix Exponentiation)
				while (exp > 0) {
					// 지수가 홀수라면 현재까지의 base 행렬을 res 행렬에 곱함 (res = res * base)
					if (exp % 2 == 1) {
						long nextR00 = (r00 * b00 + r01 * b10) % MOD;
						long nextR01 = (r00 * b01 + r01 * b11) % MOD;
						long nextR10 = (r10 * b00 + r11 * b10) % MOD;
						long nextR11 = (r10 * b01 + r11 * b11) % MOD;

						r00 = nextR00;
						r01 = nextR01;
						r10 = nextR10;
						r11 = nextR11;
					}

					// base 행렬을 제곱함 (base = base * base)
					long nextB00 = (b00 * b00 + b01 * b10) % MOD;
					long nextB01 = (b00 * b01 + b01 * b11) % MOD;
					long nextB10 = (b10 * b00 + b11 * b10) % MOD;
					long nextB11 = (b10 * b01 + b11 * b11) % MOD;

					b00 = nextB00;
					b01 = nextB01;
					b10 = nextB10;
					b11 = nextB11;

					// 지수를 절반으로 줄임
					exp /= 2;
				}
}

피보나치 행렬

피보나치 행렬

진짜 2x2 행렬 N 제곱하니 좌상단부터 우하단까지 N+1, N, N, N-1 의 피보나치 수열이 나온다.

행렬 분할 정복 - 항등원 (14를 예시로 해봤다.)

행렬 분할 정복 방식

행렬 분할 방식을 사용하니 1 -> 4 -> 8 순서로 계산된다.

7. 회고 및 배운 점

기존에는 한 가지의 방법으로만 구현하고 다른 방법은 생각해보지 않았는데 장단점을 정리하여 작성하고, 다른 방식들도 구현해보니 다른 방식과의 장단점을 잘 이해할 수 있었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.