/<> 백준 11444: 피보나치 수 6 - C#

Darkpppet·2021년 7월 4일
0

백준

목록 보기
1/4

/<> 백준 11444: 피보나치 수 6

https://www.acmicpc.net/problem/11444

solved.ac

  • Gold III
  • CLASS 4 [ESSENTIAL]

문제

피보나치 수는 0과 1로 시작한다. 0번째 피보나치 수는 0이고, 1번째 피보나치 수는 1이다. 그 다음 2번째 부터는 바로 앞 두 피보나치 수의 합이 된다.

이를 식으로 써보면 Fn = Fn-1 + Fn-2 (n ≥ 2)가 된다.

n=17일때 까지 피보나치 수를 써보면 다음과 같다.

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597

n이 주어졌을 때, n번째 피보나치 수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 n이 주어진다. n은 1,000,000,000,000,000,000보다 작거나 같은 자연수이다.

출력

첫째 줄에 n번째 피보나치 수를 1,000,000,007으로 나눈 나머지를 출력한다.

예제 입력

1000

예제 출력

517691607

코드 (채점번호 30612370)

https://www.acmicpc.net/source/30612370

using System;
using System.Numerics;
using System.Collections.Generic;

Dictionary<BigInteger, long> memo = new()
{
    [0] = 0,
    [1] = 1
};

long Fibonacci(BigInteger num)
{
    if (!memo.ContainsKey(num))
    {
        if (num % 2 == 0)
        {
            long fnM1 = Fibonacci(num / 2 - 1);
            long fn = Fibonacci(num / 2);
			
            memo[num] = (fn * (2 * fnM1 + fn)) % 1_000_000_007;
        }
        else
        {
            long fnP1 = Fibonacci(num / 2 + 1);
            long fn = Fibonacci(num / 2);
			
            memo[num] = (fn * fn + fnP1 * fnP1) % 1_000_000_007;
        }
    }
	
    return memo[num];
}

Console.WriteLine(Fibonacci(BigInteger.Parse(Console.ReadLine())));

설명

이 문제가 다른 피보나치 문제랑 다른 점은 입력 n값이 매우 크다는 것이다(최대 100경)
따라서 평소처럼

피보나치 수열의 점화식

Fn=Fn1+Fn2F_{n}=F_{n-1}+F_{n-2}

을 재귀로 구현했다간 스택 오버플로우가 발생하고 말 것이다.
그래서 이번 문제에서는 도가뉴 항등식(d'Ocagne's identity)을 사용할것이다.

도가뉴 항등식

Fm+n=Fm1Fn+FmFn+1F_{m+n}=F_{m-1}F_{n}+F_{m}F_{n+1}

이 항등식을 잘 변형하면

Fn+n=Fn1Fn+FnFn+1F_{n + n}=F_{n-1}F_{n}+F_{n}F_{n+1}
F2n=Fn(Fn1+Fn+1)=Fn(Fn1+Fn+Fn1)=Fn(Fn+2Fn1)\Rightarrow F_{2n}=F_{n}(F_{n-1}+F_{n+1})=F_{n}(F_{n-1}+F_{n}+F_{n-1})=F_{n}(F_{n}+2F_{n-1})

Fn+n+1=FnFn+Fn+1Fn+1=Fn2+Fn+12F_{n + n+1}=F_{n}F_{n}+F_{n+1}F_{n+1}=F_{n}^2+F_{n+1}^2
F2n+1=Fn+12+Fn2\Rightarrow F_{2n+1}=F_{n+1}^2+F_{n}^2

아래와 같은 새로운 점화식을 얻을 수 있다.

새로운 피보나치 수열의 점화식

F2n=Fn(Fn+2Fn1)F_{2n}=F_{n}(F_{n}+2F_{n-1})
F2n+1=Fn+12+Fn2F_{2n+1}=F_{n+1}^2+F_{n}^2

이렇게 (한쪽 항만 쭉 호출하는 것을 고려할 때) O(n)O(n)번 호출하던 것이 O(logn)O(log\,n)번만 호출해도 되도록 바뀌었다!

이제 이를 메소드로 구현 해 보면 아래와 같다.

long Fibonacci(BigInteger num)
{
    if (!memo.ContainsKey(num)) //값이 저장되어 있지 않으면 점화식을 통해 값을 구해준다.
    {
        if (num % 2 == 0) //F_2n
        {
            long fnM1 = Fibonacci(num / 2 - 1); //F_n-1
            long fn = Fibonacci(num / 2); //F_n
			
            memo[num] = (fn * (2 * fnM1 + fn)) % 1_000_000_007;
        }
        else //F_2n+1
        {
            long fnP1 = Fibonacci(num / 2 + 1); //F_n+1
            long fn = Fibonacci(num / 2); //F_n
			
            memo[num] = (fn * fn + fnP1 * fnP1) % 1_000_000_007;
        }
    }
	
    return memo[num];
}

여담으로 fn * (2 * fnM1 + fn)에서
최대 결과값은 31,000,000,0062<3260<22260<2633\cdot1,000,000,006^2<3\cdot 2^{60}<2^2\cdot2^{60}<2^{63}(long)로
long자료형으로도 오버플로우를 일으키지 않고 충분히 담을 수 있다.
마찬가지로 fn * fn + fnP1 * fnP1에서의 최대 결과값은 21,000,000,00622\cdot1,000,000,006^2로 문제 없다.

추가적으로 피보나치 문제들의 기본이 되는 메모이제이션은 이번 문제에선 설명하지 않겠다.

0개의 댓글