정화 코딩

[C++] 피보나치 수 시리즈 본문

PS

[C++] 피보나치 수 시리즈

jungh150c 2024. 11. 9. 02:03

솔브닥 class 4+ 풀고 있었는데 거기에 피보나치 수 6 문제가 있길래 이참에 피보나치 수 문제를 쭉 풀어볼까 한다.

 


피보나치 수 (백준 2747번)

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

 

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    
    vector<int> fib(n + 1);
    fib[0] = 0;
    fib[1] = 1;
    for (int i = 2; i < n + 1; i++) {
        fib[i] = fib[i - 1] + fib[i - 2];
    }

    cout << fib[n] << '\n';
}

바텀업 dp로 풀었다. (AC)

 


피보나치 수 2 (백준 2748번)

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

 

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    
    vector<long long> fib(n + 1);
    fib[0] = 0;
    fib[1] = 1;
    for (int i = 2; i < n + 1; i++) {
        fib[i] = fib[i - 1] + fib[i - 2];
    }

    cout << fib[n] << '\n';
}

나머지는 동일한데 n 크기만 조금 더 커졌다. 1번 문제에서 제출한 코드에 90을 입력하면 오버플로우가 나기 때문에 fib 배열의 타입을 long long으로 바꿔주었다. (AC)

 


피보나치 수 3 (백준 2749번)

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

 

#include <iostream>
using namespace std;

const int M = 1000000;
const int PI = 15 * (M / 10);
int fib[PI];

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    long long n;
    cin >> n;
    
    fib[0] = 0;
    fib[1] = 1;
    for (int i = 2; i < PI; i++) {
        fib[i] = (fib[i - 1] + fib[i - 2]) % M;
    }

    cout << fib[n % PI] << '\n';
}

2번 문제보다 n 크기가 훨씬 커졌다. 어떻게 풀어야 하는지 감이 안 와서 질문 게시판을 슬쩍 보니 피사노 주기라는 것이 있었다. 

피사노 주기(Pisano Period)는 피보나치 수열을 어떤 정수 m으로 나눈 나머지들의 반복되는 패턴의 길이를 의미한다. 모든 m에 대해 이 주기는 존재하며, 피보나치 수열을 m으로 나누었을 때의 나머지는 일정한 패턴으로 반복된다.
일반적으로 m에 대한 주기는 규칙이 나올 때까지 구해보며 직접 찾아야 하지만, m = 10^k (k>2) 형태의 경우에는 주기가 항상 15*10^(k-1)로 고정된다. 

이 문제도 이 형태에 포함되기 때문에 주기를 고정적으로 구할 수 있다. 

주의할 점은 어차피 1,000,000의 나머지이기 때문에 fib 배열의 타입은 int로 해도 되지만, 마지막에서만 M으로 나눠주면 오버플로우가 날 수 있기 때문에 fib[i]를 계산할 때마다 나눠주어야 한다. 또 당연한 거지만 입력받을 때 n을 long long으로 입력받아야 한다! (AC)

 


피보나치 수 4 (백준 10826번)

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

 

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

string addBigNum(string a, string b) {
    int carry = 0;
    string res = "";

    while (!a.empty() || !b.empty() || carry) {
        if (!a.empty()) {
            carry += a.back() - '0';
            a.pop_back();
        }
        if (!b.empty()) {
            carry += b.back() - '0';
            b.pop_back();
        }

        res += ((carry % 10) + '0');
        carry /= 10;
    }

    reverse(res.begin(), res.end());
    return res;
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    
    vector<string> fib(n + 1);
    fib[0] = "0";
    if (n != 0) fib[1] = "1";
    for (int i = 2; i < n + 1; i++) {
        fib[i] = addBigNum(fib[i - 1], fib[i - 2]);
    }

    cout << fib[n] << '\n';
}

2번 문제보다 n이 커졌지만 나머지를 구하는 문제는 아니다. 2번 문제에서 제출한 코드에 90부터 입력을 넣어보면 93부터 오버플로우가 나는 것을 확인할 수 있다. long long으로도 오버플로우가 날만큼 큰 수이기 때문에 문자열로 덧셈 연산을 구현해주어야 한다. 

참고로 reverse는 문자열의 앞뒤를 뒤집는 함수이고 <algorithm> 헤더 파일에 있다. (AC)

[참고] https://changicho.tistory.com/entry/%ED%81%B0-%EC%88%98-%EC%97%B0%EC%82%B0-Big-Integer

 

string addBigNum(string a, string b) {
    int carry = 0;
    string res = "";

    while (!a.empty() || !b.empty() || carry) {
        if (!a.empty()) {
            carry += a.back() - '0';
            a.pop_back();
        }
        if (!b.empty()) {
            carry += b.back() - '0';
            b.pop_back();
        }

        res = char((carry % 10) + '0') + res;
        carry /= 10;
    }

    return res;
}

처음에는 이렇게 새로운 수를 res 맨 앞에 붙이면 마지막에 뒤집는 과정 없어도 되니까 더 편하지 않을까 싶었다. 하지만 이렇게 하면 새로운 수를 res 맨 앞에 붙일 때마다 복사가 일어나므로 훨씬 느리다.

 

실제로 10배 정도 차이가 나는 것을 볼 수 있다.

 


피보나치 수 5 (백준 10870번)

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

 

#include <iostream>
using namespace std;

int fib(int n) {
    if (n == 0) return 0;
    else if (n == 1) return 1;
    else return fib(n - 1) + fib(n - 2);
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    cout << fib(n) << '\n';
}

오히려 1번 문제보다 n이 작다. 재귀로도 통과될 것 같아서 재귀로 짜봤다. 당연히 dp로도 통과된다. (AC)

 


피보나치 수 6 (백준 11444번)

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

 

#include <iostream>
#include <vector>
using namespace std;

const int M = 1000000007;
vector<int> fib;

int findPisanoPeriod(int m) {
    fib.push_back(0);
    fib.push_back(1);
    int idx = 2;
    while (1) {
        fib.push_back((fib[idx - 1] + fib[idx - 2]) % m);
        if (fib[idx] == 1 && fib[idx - 1] == 0) {
            break;
        }
        idx++;
    }
    return idx - 1;
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    long long n;
    cin >> n;

    int pi = findPisanoPeriod(M);
    cout << fib[n % pi] << '\n';
}

3번 문제와 비슷하지만 이번에는 나누는 수가 10의 거듭제곱 형태가 아니라 주기를 바로 계산할 수 없다. 그래서 이렇게 직접 주기를 구하도록 코드를 작성해보았다. 그런데 M이 너무 커서 그런지 마치 무한루프를 도는 것 같았다. 

 

#include <iostream>
#include <vector>
#include <map>
using namespace std;

int mod = 1000000007;
map<long long, long long> m;

long long fib(long long n) {
    if (n == 0) return 0;
    else if (n == 1) return 1;
    else if (n == 2) return 1;
    else {
        if (m.find(n) != m.end()) {
            return m[n];
        } else {
            if (n % 2 == 0) {
                long long tmp = (fib(n / 2) * (fib(n / 2 + 1) + fib(n / 2 - 1))) % mod;
                m[n] = tmp;
                return tmp;
            } else {
                long long tmp = (fib((n + 1) / 2) * fib((n + 1) / 2) + fib((n - 1) / 2) * fib((n - 1) / 2)) % mod;
                m[n] = tmp;
                return tmp;
            }
        }
    }
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    long long n;
    cin >> n;
    cout << fib(n) << '\n';
}

그래서 찾아보니 M이 너무 크기 때문에 점화식을 변형해서 2로 분할되는 점화식을 유도할 수 있는 것을 알게 되었다.

f(n) = f(n-1) + f(n-2) = f(2)f(n-1) + f(1)f(n-2)
      = 2f(n-2) + f(n-3) = f(3)f(n-2) + f(2)f(n-3)
       = 3f(n-3) + 2f(n-4) = f(4)f(n-3) + f(3)f(n-4)
       = 5f(n-4) + 3f(n-5) = f(5)f(n-4) + f(4)f(n-5)
       = ...
       = f(k+1)f(n-k) + f(k)f(n-k-1)

n이 짝수일 때, 즉 n=2k일 때, f(2k) = f(k){f(k+1)+f(k-1)}
n이 홀수일 때, 즉 n=2k+1일 때, f(2k+1) = {f(k+1)}^2 + {f(k)}^2

[참고] https://howudong.tistory.com/300

추가로, 탑다운으로 하려면 메모이제이션을 해야 한다. 이 때 M이 너무 크기 때문에 map을 사용하였다. (AC)

 


피보나치 수 7 (백준 15624번)

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

 

#include <iostream>
#include <vector>
using namespace std;

int mod = 1000000007;

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int n;
    cin >> n;
    
    vector<long long> fib(n + 1);
    fib[0] = 0;
    fib[1] = 1;
    for (int i = 2; i < n + 1; i++) {
        fib[i] = (fib[i - 1] + fib[i - 2]) % mod;
    }

    cout << fib[n] << '\n';
}

2번 문제의 코드를 그대로 사용하되 1,000,000,007로 나눠주는 부분만 추가해줬다. (AC)

 

'PS' 카테고리의 다른 글

[C++] 웜홀 (백준 1865번)  (1) 2024.11.12
[C++] 열쇠 (백준 9328번)  (0) 2024.11.10
[C++] RGB거리 2 (백준 17404번)  (0) 2024.11.05
[C++] 팰린드롬? (백준 10942번)  (1) 2024.10.16
[C++] 스도쿠 (백준 2239번)  (0) 2024.10.14
Comments