Problem: Write the code for nth Fibonacci number.
Solution: There are basically two approaches to solving the Fibonacci problem. Lets looks at the definition of Fibonacci series first.
The Fibonacci series is defined as follows
So, F(n) = F(n-1)+F(n-2) for n≥2and 1 otherwise.
There are couple of approach to solve this.
Solution 1 - Using Recursion
If we translate that to the C language we get:
Time Complexity: T(n) = T(n-1) + T(n-2) + θ(1) which is exponential.
We can observe that this implementation does a lot of repeated work (see the following recursion tree). So this is a bad implementation for nth Fibonacci number.
Solving the recurrence - T(n) ≥ 2 T(n-2)
Now its simpler. We keep on multiplying until T(n-2) reduces to T(1) and this can happen in n/2 steps as we can subtract 2 from n, n/2 times.
Hence T(n) ≥ 2n/2 * constant
T(n) = θ (2 n/2)
Extra Space: O(n) if we consider the fuinction call stack size, otherwise O(1).
Solution 2 - Top Down Dynamic Programming Solution
Now, whats the running time of the above program. It's horrible!! It's exponential. Why? We have a lot of duplicate work being done on each recursive call.
Typically when you are doing duplicate work, Dynamic Programming can come to rescue. Now, I don't want to go into too much detail on dynamic programming, but you have 2 approaches to Dynamic Programming. One is the bottom up approach and the second is called the top down approach. Calculating F(n) given F(n-1) would be a bottom up approach and calculating F(n) by recursively calling F(n-1) and F(n-2) would be top down. For a thorough treatment of dynamic programming see a Algorithms text or this wikipedia entry.
We can improve the above RecursiveFibonacci function by caching results of previous calculations. Doing this greatly reduces the number of recursive calls and improves the program efficiency. This is an example of top down dynamic programming.
Memoization
Now in DP, we will use something called memorization, which is similar to cache above. We hold a memo of what state we have went through, and hence we end something like this:
So, we see memoization is very similar to what we were talking about caching. So, we are calling fib(k) only once rather and saving it, rather than computing it again and again whenever fib(k) is called in case of recursive solution.
So, lets assume that each time we call DPFibonacci, we have recursion and as it is happening only once, lets assume its constant time, and for each element we are only calling recursion once, and hence time complexity here is O(n). This is top down approach.
Solution 3 -Bottom up DP
Now lets look at the bottom up dynamic programming approach. For a given n we would need to have F(n-1), F(n-2), F(n-3) ... F(0) already solved. In this case we'll start at the bottom and work our way up.
We could have used a cache like we used in the top down approach, but all we really need is the last 2 results. We can get away with just using 2 variables. This is also an iterative solution which is better compared to the recursive one because of the absence of the stack overhead involved in recursive calls.
Time Complexity: O(n)
Extra Space: O(1)
Method 4 ( Using power of the matrix {{1,1},{1,0}} )
This another O(n) which relies on the fact that if we n times multiply the matrix M = {{1,1},{1,0}} to itself (in other words calculate power(M, n )), then we get the (n+1)th Fibonacci number as the element at row and column (0, 0) in the resultant matrix.
The matrix representation gives the following closed expression for the Fibonacci numbers:
ime Complexity: O(n)
Extra Space: O(1)
Method 5 ( Optimized Method 4 )
The method 4 can be optimized to work in O(Logn) time complexity. We can do recursive multiplication to get power(M, n) in the prevous method (Similar to the optimization done in this post)
Time Complexity: O(log n)
Extra Space: O(Logn) if we consider the function call stack size, otherwise O(1).
Source - http://www.geeksforgeeks.org
Solution: There are basically two approaches to solving the Fibonacci problem. Lets looks at the definition of Fibonacci series first.
The Fibonacci series is defined as follows
F(n) = 0 for n = 0 1 for n = 1 F(n-1) + F(n-2) for n > 1
So, F(n) = F(n-1)+F(n-2) for n≥2and 1 otherwise.
There are couple of approach to solve this.
Solution 1 - Using Recursion
If we translate that to the C language we get:
int RecursiveFibonacci(int n) { if(n == 0) return 0; if(n == 1) return 1; return RecursiveFibonacci(n-1) + RecursiveFibonacci(n-2); }
Time Complexity: T(n) = T(n-1) + T(n-2) + θ(1) which is exponential.
We can observe that this implementation does a lot of repeated work (see the following recursion tree). So this is a bad implementation for nth Fibonacci number.
fib(5) / \ fib(4) fib(3) / \ / \ fib(3) fib(2) fib(2) fib(1) / \ / \ / \ fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) / \ fib(1) fib(0)
Solving the recurrence - T(n) ≥ 2 T(n-2)
Now its simpler. We keep on multiplying until T(n-2) reduces to T(1) and this can happen in n/2 steps as we can subtract 2 from n, n/2 times.
Hence T(n) ≥ 2n/2 * constant
T(n) = θ (2 n/2)
Extra Space: O(n) if we consider the fuinction call stack size, otherwise O(1).
Solution 2 - Top Down Dynamic Programming Solution
Now, whats the running time of the above program. It's horrible!! It's exponential. Why? We have a lot of duplicate work being done on each recursive call.
Typically when you are doing duplicate work, Dynamic Programming can come to rescue. Now, I don't want to go into too much detail on dynamic programming, but you have 2 approaches to Dynamic Programming. One is the bottom up approach and the second is called the top down approach. Calculating F(n) given F(n-1) would be a bottom up approach and calculating F(n) by recursively calling F(n-1) and F(n-2) would be top down. For a thorough treatment of dynamic programming see a Algorithms text or this wikipedia entry.
We can improve the above RecursiveFibonacci function by caching results of previous calculations. Doing this greatly reduces the number of recursive calls and improves the program efficiency. This is an example of top down dynamic programming.
int RecursiveFibonacci(int n) { if(n == 0) return 0; if(n == 1) return 1; if(cache[n] == null) cache[n] = RecursiveFibonacci(n-1) + RecursiveFibonacci(n-2); return cache[n]; }
Memoization
Now in DP, we will use something called memorization, which is similar to cache above. We hold a memo of what state we have went through, and hence we end something like this:
memo = {} //empty dictionary int DPFibonacci(int n) { if(n in Memo) return Memo[n]; if(n == 1) return 1; f = DPFibonacci(n-1)+DPFibonacci(n-2) Memo[n] = f; return Memo[n]; }
So, we see memoization is very similar to what we were talking about caching. So, we are calling fib(k) only once rather and saving it, rather than computing it again and again whenever fib(k) is called in case of recursive solution.
So, lets assume that each time we call DPFibonacci, we have recursion and as it is happening only once, lets assume its constant time, and for each element we are only calling recursion once, and hence time complexity here is O(n). This is top down approach.
Solution 3 -Bottom up DP
Now lets look at the bottom up dynamic programming approach. For a given n we would need to have F(n-1), F(n-2), F(n-3) ... F(0) already solved. In this case we'll start at the bottom and work our way up.
int GetFibonacci(int n) { if(n == 0) return 0; if(n == 1) return 1; last1 = 0; last2 = 1; ans = 0; for(int i=0; i < n-1; i++) { ans = last1 + last2; last1 = last2; last2 = ans; } return ans; }
We could have used a cache like we used in the top down approach, but all we really need is the last 2 results. We can get away with just using 2 variables. This is also an iterative solution which is better compared to the recursive one because of the absence of the stack overhead involved in recursive calls.
Time Complexity: O(n)
Extra Space: O(1)
Method 4 ( Using power of the matrix {{1,1},{1,0}} )
This another O(n) which relies on the fact that if we n times multiply the matrix M = {{1,1},{1,0}} to itself (in other words calculate power(M, n )), then we get the (n+1)th Fibonacci number as the element at row and column (0, 0) in the resultant matrix.
The matrix representation gives the following closed expression for the Fibonacci numbers:
#include <stdio.h> /* Helper function that multiplies 2 matricies F and M of size 2*2, and puts the multiplication result back to F[][] */ void multiply(int F[2][2], int M[2][2]); /* Helper function that calculates F[][] raise to the power n and puts the result in F[][] Note that this function is desinged only for fib() and won't work as general power function */ void power(int F[2][2], int n); int fib(int n) { int F[2][2] = {{1,1},{1,0}}; if (n == 0) return 0; power(F, n-1); return F[0][0]; } void multiply(int F[2][2], int M[2][2]) { int x = F[0][0]*M[0][0] + F[0][1]*M[1][0]; int y = F[0][0]*M[0][1] + F[0][1]*M[1][1]; int z = F[1][0]*M[0][0] + F[1][1]*M[1][0]; int w = F[1][0]*M[0][1] + F[1][1]*M[1][1]; F[0][0] = x; F[0][1] = y; F[1][0] = z; F[1][1] = w; } void power(int F[2][2], int n) { int i; int M[2][2] = {{1,1},{1,0}}; // n - 1 times multiply the matrix to {{1,0},{0,1}} for (i = 2; i <= n; i++) multiply(F, M); } /* Driver program to test above function */ int main() { int n = 9; printf("%d", fib(n)); getchar(); return 0; }
ime Complexity: O(n)
Extra Space: O(1)
Method 5 ( Optimized Method 4 )
The method 4 can be optimized to work in O(Logn) time complexity. We can do recursive multiplication to get power(M, n) in the prevous method (Similar to the optimization done in this post)
#include <stdio.h> void multiply(int F[2][2], int M[2][2]); void power(int F[2][2], int n); /* function that returns nth Fibonacci number */ int fib(int n) { int F[2][2] = {{1,1},{1,0}}; if (n == 0) return 0; power(F, n-1); return F[0][0]; } /* Optimized version of power() in method 4 */ void power(int F[2][2], int n) { if( n == 0 || n == 1) return; int M[2][2] = {{1,1},{1,0}}; power(F, n/2); multiply(F, F); if (n%2 != 0) multiply(F, M); } void multiply(int F[2][2], int M[2][2]) { int x = F[0][0]*M[0][0] + F[0][1]*M[1][0]; int y = F[0][0]*M[0][1] + F[0][1]*M[1][1]; int z = F[1][0]*M[0][0] + F[1][1]*M[1][0]; int w = F[1][0]*M[0][1] + F[1][1]*M[1][1]; F[0][0] = x; F[0][1] = y; F[1][0] = z; F[1][1] = w; } /* Driver program to test above function */ int main() { int n = 9; printf("%d", fib(9)); getchar(); return 0; }
Time Complexity: O(log n)
Extra Space: O(Logn) if we consider the function call stack size, otherwise O(1).
Source - http://www.geeksforgeeks.org
0 comments:
Post a Comment