Home > front end >  How to optimize Fibonacci using memoization in C ?
How to optimize Fibonacci using memoization in C ?

Time:11-10

I'm struggling with getting the correct implementation of finding the nth number in the Fibonacci sequence. More accurately, how to optimize it with DP.

I was able to correctly write it in the bottom-up approach:

int fib(int n) {
    int dp[n   2];
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; i  )
        dp[i] = dp[i - 1]   dp[i - 2];
    return dp[n];
}

But the top-down (memoized) approach is harder for me to grasp. This is what I put originally:

int fib(int n) {
    std::vector<int> dp(n   1, -1);

    // Lambda Function
    auto recurse = [&](int n) {
        if (dp[n] != -1)
            return dp[n];

        if (n == 0) {
            dp[n] = 0;
            return 0;
        }
        if (n == 1){
            dp[n] = 1;
            return 1;
        }
            
        dp[n] = fib(n - 2)   fib(n - 1);
        return dp[n];
    };

    recurse(n);
    return dp.back();
}

...This was my first time using a C lambda function, though, so I also tried:

int recurse(int n, std::vector<int>& dp) {
    if (dp[n] != -1)
        return dp[n];

    if (n == 0) {
        dp[n] = 0;
        return 0;
    }
    if (n == 1){
        dp[n] = 1;
        return 1;
    }
      
    dp[n] = fib(n - 2)   fib(n - 1);
    return dp[n];
}

int fib(int n) {
    std::vector<int> dp(n   1, -1);
    recurse(n, dp);
    return dp.back();
}

...just to make sure. Both of these gave time limit exceeded errors on leetcode. This, obviously, seemed off, since DP/Memoization are optimization techniques. Is my top-down approach wrong? How do I fix it?

CodePudding user response:

Whenever you call fib(n), you create new vector dp with all element -1, then in fact, you don't memorize any previous result.

int fib(int n) {
    std::vector<int> dp(n   1, -1);

To solve this issue, you can declare dp vector outside the function as fib as shared data. Note that you said this is leetcode problem, then you can declare a vector with fixed size. For example,

std::vector<int> dp = std::vector<int>(31, -1);

You can test yourself. I just give the code which is runable in leetcode.

int fib(int n) {
    
// Lambda Function
auto recurse = [&](int n) {        
    if (n == 0) {
        dp[n] = 0;
        return 0;
    }
    if (n == 1){
        dp[n] = 1;
        return 1;
    }
    
    if (dp[n] != -1)
        return dp[n];
        
    dp[n] = fib(n - 2)   fib(n - 1);
    return dp[n];
};
recurse(n);
return dp[n];
}

CodePudding user response:

Very interesting. I will show you the fastest possible solution with an additional low memory footprint.

For this we will use compile time memoization.

One important property of the Fibonacci series is that the values grow strongly exponential. So, all existing build in integer data types will overflow rather quick.

With Binet's formula you can calculate that the 93rd Fibonacci number is the last that will fit in a 64bit unsigned value.

And calculating 93 values during compilation is a really simple and fast task.

So, how to do?

We will first define the default approach for calculation a Fibonacci number as a constexpr function. Iterative and non recursive.

// Constexpr function to calculate the nth Fibonacci number
constexpr unsigned long long getFibonacciNumber(size_t index) noexcept {
    // Initialize first two even numbers 
    unsigned long long f1{ 0 }, f2{ 1 };

    // calculating Fibonacci value 
    while (index--) {
        // get next value of Fibonacci sequence 
        unsigned long long f3 = f2   f1;
        // Move to next number
        f1 = f2;
        f2 = f3;
    }
    return f2;
}

With that, Fibonacci numbers can easily be calculated at compile time. Then, we fill a std::array with all Fibonacci numbers. We use also a constexpr and make it a template with a variadic parameter pack.

We use std::integer_sequence to create a Fibonacci number for indices 0,1,2,3,4,5, ....

That is straigtforward and not complicated:

template <size_t... ManyIndices>
constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept {
    return std::array<unsigned long long, sizeof...(ManyIndices)>{ { getFibonacciNumber(ManyIndices)... } };
};

This function will be fed with an integer sequence 0,1,2,3,4,... and return a std::array<unsigned long long, ...> with the corresponding Fibonacci numbers.

We know that we can store maximum 93 values. And therefore we make a next function, that will call the above with the integer sequence 1,2,3,4,...,92,93, like so:

constexpr auto generateArray() noexcept {
    return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
}

And now, finally,

constexpr auto FIB = generateArray();

will give us a compile-time std::array<unsigned long long, 93> with the name FIB containing all Fibonacci numbers. And if we need the i'th Fibonacci number, then we can simply write FIB[i]. There will be no calculation at runtime.

I do not think that there is a faster or easier way to calculate the n'th Fibonacci number.

Please see the complete program below:

#include <iostream>
#include <array>
#include <utility>
// ----------------------------------------------------------------------
// All the following will be done during compile time

// Constexpr function to calculate the nth Fibonacci number
constexpr unsigned long long getFibonacciNumber(size_t index) {
    // Initialize first two even numbers 
    unsigned long long f1{ 0 }, f2{ 1 };

    // calculating Fibonacci value 
    while (index--) {
        // get next value of Fibonacci sequence 
        unsigned long long f3 = f2   f1;
        // Move to next number
        f1 = f2;
        f2 = f3;
    }
    return f2;
}
// We will automatically build an array of Fibonacci numberscompile time
// Generate a std::array with n elements 
template <size_t... ManyIndices>
constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept {
    return std::array<unsigned long long, sizeof...(ManyIndices)>{ { getFibonacciNumber(ManyIndices)... } };
};

// Max index for Fibonaccis that for in an 64bit unsigned value (Binets formula)
constexpr size_t MaxIndexFor64BitValue = 93;

// Generate the required number of elements
constexpr auto generateArray()noexcept {
    return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
}

// This is an constexpr array of all Fibonacci numbers
constexpr auto FIB = generateArray();
// ----------------------------------------------------------------------

// Test
int main() {

    // Print all possible Fibonacci numbers
    for (size_t i{}; i < MaxIndexFor64BitValue;   i)

        std::cout << i << "\t--> " << FIB[i] << '\n';

    return 0;
}

Developed and tested with Microsoft Visual Studio Community 2019, Version 16.8.2.

Additionally compiled and tested with clang11.0 and gcc10.2

Language: C 17

CodePudding user response:

You set up the memoization storage in fib, and then create the recursive part of your solution in the lambda recurse. That means that here: dp[n] = fib(n - 2) fib(n - 1); you really should call recurse not fib. But in order to do that with a lambda, you need to give the lambda to the lambda so to speak.

Example:

#include <iostream>
#include <vector>

int fib(int n)
{
    std::vector<int> dp(n   1, -1);

    // Lambda Function
    auto recurse = [&dp](int n, auto &rec)
    {
        if (dp[n] != -1)
            return dp[n];

        if (n == 0)
        {
            dp[n] = 0;
            return 0;
        }
        if (n == 1)
        {
            dp[n] = 1;
            return 1;
        }

        dp[n] = rec(n - 2, rec)   rec(n - 1, rec);
        return dp[n];
    };

    recurse(n, recurse);
    return dp.back();
}

int main()
{
    std::cout << fib(12) << '\n';
}
  • Related