|
| 1 | +<h1 align="center">Nth - Fibonacci - Number - Example</h1> |
| 2 | + |
| 3 | +## Problem Statement |
| 4 | +**Problem URL :** [Nth Fibonacci Number](https://www.geeksforgeeks.org/problems/nth-fibonacci-number1335/1?itm_source=geeksforgeeks&itm_medium=article&itm_campaign=practice_card) |
| 5 | + |
| 6 | + |
| 7 | + |
| 8 | +## Problem Explanation |
| 9 | +The task is to find the Nth Fibonacci number. The Fibonacci sequence is a series of numbers where each number is the sum of the two preceding ones, starting from 0 and 1. Therefore, the sequence starts: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, and so on. |
| 10 | + |
| 11 | +You are required to write a function to compute the Nth Fibonacci number in this sequence. |
| 12 | + |
| 13 | +### What is a Fibonacci Number? |
| 14 | +The Fibonacci sequence is defined as: |
| 15 | +- F(0) = 0 |
| 16 | +- F(1) = 1 |
| 17 | +- For n > 1, F(n) = F(n-1) + F(n-2) |
| 18 | + |
| 19 | +This means that each number in the sequence is the sum of the two preceding ones. |
| 20 | + |
| 21 | +### Calculation of Fibonacci Numbers: |
| 22 | +Let's break it down step-by-step. |
| 23 | + |
| 24 | +#### Step 1: Base Cases |
| 25 | +There are two base cases in the Fibonacci sequence: |
| 26 | +- If N = 0, the Fibonacci number is 0. |
| 27 | +- If N = 1, the Fibonacci number is 1. |
| 28 | + |
| 29 | +#### Step 2: Recursive Definition |
| 30 | +For any other number N (N > 1), the Fibonacci number is defined as the sum of the two preceding numbers: |
| 31 | +- F(N) = F(N-1) + F(N-2) |
| 32 | + |
| 33 | +## What is Dynamic Programming |
| 34 | +Dynamic Programming (DP) is a problem-solving technique used to solve problems with overlapping subproblems and optimal substructure. It avoids redundant computations by storing the results of subproblems, making it more efficient than plain recursion. |
| 35 | + |
| 36 | +### **Core Concepts of Dynamic Programming** |
| 37 | + |
| 38 | +#### 1. **Overlapping Subproblems** |
| 39 | + Subproblems are solved multiple times during the computation. |
| 40 | + Example: |
| 41 | + Computing Fibonacci numbers recursively involves solving \( F(n-1) \) and \( F(n-2) \), but \( F(n-1) \) and \( F(n-2) \) have common subproblems like \( F(n-3) \), leading to redundant work. |
| 42 | + |
| 43 | +#### 2. **Optimal Substructure** |
| 44 | + A problem exhibits optimal substructure if its solution can be derived from solutions of smaller subproblems. |
| 45 | + Example: |
| 46 | + The shortest path between two nodes in a graph can be computed by finding the shortest paths between intermediate nodes. |
| 47 | + |
| 48 | +### **Three Key DP Techniques** |
| 49 | + |
| 50 | +### **1. Top-Down Approach (Recursion + Memoization)** |
| 51 | + |
| 52 | +The top-down approach involves solving problems recursively while storing results of already-computed subproblems in a cache or a table. This process is called **memoization**. |
| 53 | + |
| 54 | +- **How It Works**: |
| 55 | + - Start solving the problem from the largest input. |
| 56 | + - Store the result of each subproblem in a memoization table. |
| 57 | + - Before solving a subproblem, check the table to see if it has already been solved. |
| 58 | + |
| 59 | +- **Example: Fibonacci Sequence** |
| 60 | + The Fibonacci sequence is defined as: |
| 61 | + \( F(n) = F(n-1) + F(n-2) \), where \( F(0) = 0, F(1) = 1 \). |
| 62 | + |
| 63 | +##### Source Code |
| 64 | +```cpp |
| 65 | +#include <vector> |
| 66 | +using namespace std; |
| 67 | + |
| 68 | +class Solution { |
| 69 | + public: |
| 70 | + int fibonacci(int n, vector<int>& dp) { |
| 71 | + // Base cases |
| 72 | + if (n == 0 || n == 1) return n; |
| 73 | + |
| 74 | + // If already computed, return the cached result |
| 75 | + if (dp[n] != -1) return dp[n]; |
| 76 | + |
| 77 | + // Compute the value recursively and store it in dp[n] |
| 78 | + dp[n] = fibonacci(n - 1, dp) + fibonacci(n - 2, dp); |
| 79 | + return dp[n]; |
| 80 | + } |
| 81 | + |
| 82 | + int nthFibonacci(int n) { |
| 83 | + vector<int> dp(n + 1, -1); // Initialize memoization table |
| 84 | + return fibonacci(n, dp); |
| 85 | + } |
| 86 | +}; |
| 87 | +``` |
| 88 | +
|
| 89 | +- **Advantages**: Efficient compared to plain recursion due to reduced recomputation. |
| 90 | +- **Disadvantages**: Uses extra space for the memoization table. |
| 91 | +
|
| 92 | +
|
| 93 | +
|
| 94 | +### **2. Bottom-Up Approach (Tabulation)** |
| 95 | +
|
| 96 | +The bottom-up approach involves solving all subproblems iteratively, starting from the smallest subproblems and building up to the desired solution. This is called **tabulation** because it uses a table to store intermediate results. |
| 97 | +
|
| 98 | +- **How It Works**: |
| 99 | + - Define a table (array or matrix) to store the results of subproblems. |
| 100 | + - Solve the smallest subproblem first, then use its result to solve larger subproblems. |
| 101 | +
|
| 102 | +- **Example: Fibonacci Sequence** |
| 103 | +
|
| 104 | +##### Source Code |
| 105 | +```cpp |
| 106 | +#include <vector> |
| 107 | +using namespace std; |
| 108 | +
|
| 109 | +class Solution { |
| 110 | + public: |
| 111 | + int nthFibonacci(int n) { |
| 112 | + vector<int> dp(n + 1); // Table to store Fibonacci numbers |
| 113 | + dp[0] = 0; // Base case |
| 114 | + dp[1] = 1; // Base case |
| 115 | +
|
| 116 | + for (int i = 2; i <= n; i++) { |
| 117 | + dp[i] = dp[i - 1] + dp[i - 2]; // Use previously computed values |
| 118 | + } |
| 119 | +
|
| 120 | + return dp[n]; |
| 121 | + } |
| 122 | +}; |
| 123 | +``` |
| 124 | + |
| 125 | +- **Advantages**: Avoids recursion and stack overflow. |
| 126 | +- **Disadvantages**: Requires space proportional to the size of the table. |
| 127 | + |
| 128 | + |
| 129 | +### **3. Space-Optimized Approach** |
| 130 | + |
| 131 | +In many problems, only a few values from the table are required at any given time. We can optimize the space by storing only the necessary states. |
| 132 | + |
| 133 | +- **How It Works**: |
| 134 | + - Reduce the size of the table by keeping only the last few values needed for the computation. |
| 135 | + |
| 136 | +- **Example: Fibonacci Sequence** |
| 137 | + |
| 138 | +##### Source Code |
| 139 | +```cpp |
| 140 | +class Solution { |
| 141 | + public: |
| 142 | + int nthFibonacci(int n) { |
| 143 | + if (n == 0) return 0; // Base case |
| 144 | + if (n == 1) return 1; // Base case |
| 145 | + |
| 146 | + int a = 0, b = 1; // Store only the last two Fibonacci numbers |
| 147 | + int current; |
| 148 | + |
| 149 | + for (int i = 2; i <= n; i++) { |
| 150 | + current = a + b; // Compute the current Fibonacci number |
| 151 | + a = b; // Update a to the previous b |
| 152 | + b = current; // Update b to the current value |
| 153 | + } |
| 154 | + |
| 155 | + return current; |
| 156 | + } |
| 157 | +}; |
| 158 | +``` |
| 159 | + |
| 160 | +- **Advantages**: Significantly reduces space complexity. |
| 161 | +- **Disadvantages**: Slightly harder to implement compared to the tabulation approach. |
| 162 | + |
| 163 | + |
| 164 | + |
| 165 | +### **Comparison of Techniques** |
| 166 | + |
| 167 | +| **Technique** | **Space Complexity** | **Time Complexity** | **Approach** | |
| 168 | +|-|-|-|-| |
| 169 | +| **Top-Down (Memoization)**| \( O(n) \) | \( O(n) \) | Recursive with caching | |
| 170 | +| **Bottom-Up (Tabulation)**| \( O(n) \) | \( O(n) \) | Iterative, builds solution step-by-step | |
| 171 | +| **Space-Optimized** | \( O(1) \) | \( O(n) \) | Iterative, stores only needed values | |
| 172 | + |
| 173 | + |
| 174 | +## Why Not Recursive Approach |
| 175 | +##### source code |
| 176 | +```cpp |
| 177 | +class Solution { |
| 178 | + public: |
| 179 | + int nthFibonacci(int n) { |
| 180 | + // Base case: If n is 0 or 1, return n directly. |
| 181 | + // F(0) = 0 and F(1) = 1 are the first two terms of the Fibonacci sequence. |
| 182 | + if (n == 0 || n == 1) |
| 183 | + return n; |
| 184 | + |
| 185 | + // Recursive case: |
| 186 | + // The nth Fibonacci number is the sum of the (n-1)th and (n-2)th Fibonacci numbers. |
| 187 | + // Recursively calculate the values for smaller subproblems. |
| 188 | + return nthFibonacci(n - 1) + nthFibonacci(n - 2); |
| 189 | + } |
| 190 | +}; |
| 191 | +``` |
| 192 | + |
| 193 | +The **recursive approach** for solving problems like the Fibonacci sequence may seem intuitive and elegant, but it has significant drawbacks that make it inefficient for larger inputs. Let’s break this down: |
| 194 | + |
| 195 | +### **Reasons to Avoid the Recursive Approach** |
| 196 | + |
| 197 | +#### 1. **Exponential Time Complexity** |
| 198 | + - The time complexity of the recursive Fibonacci solution is **O(2^n)**. This happens because each call to the function makes two additional calls, leading to an exponential growth in the number of calls. |
| 199 | + - Example: Calculating \( F(5) \) involves computing \( F(4) \) and \( F(3) \), but \( F(4) \) also computes \( F(3) \) and \( F(2) \), leading to redundant calculations. |
| 200 | + |
| 201 | +#### 2. **Redundant Calculations** |
| 202 | + - The same subproblems are solved repeatedly. For example: |
| 203 | + - To compute \( F(5) \), you compute \( F(4) \) and \( F(3) \). |
| 204 | + - Computing \( F(4) \) requires \( F(3) \) and \( F(2) \), so \( F(3) \) is calculated twice. |
| 205 | + - This redundancy increases with \( n \), wasting computation time. |
| 206 | + |
| 207 | +#### 3. **Stack Overflow (Memory Constraints)** |
| 208 | + - Recursive calls consume stack space proportional to the depth of the recursion. For Fibonacci, the depth of the recursion is \( n \), so the space complexity is **O(n)**. |
| 209 | + - For very large \( n \), this can lead to **stack overflow errors**. |
| 210 | + |
| 211 | +#### 4. **Lack of Intermediate Storage** |
| 212 | + - In the recursive approach, no storage (memoization) is used to save the results of already-computed subproblems. This makes it inefficient compared to dynamic programming approaches. |
| 213 | + |
| 214 | + |
| 215 | +### **Comparison with Dynamic Programming** |
| 216 | + |
| 217 | +| **Aspect** | **Recursive Approach** | **Dynamic Programming** | |
| 218 | +|----------------------|------------------------------|-------------------------------------| |
| 219 | +| **Time Complexity** | \( O(2^n) \) | \( O(n) \) | |
| 220 | +| **Space Complexity** | \( O(n) \) (due to the call stack) | \( O(n) \) (or \( O(1) \) if space optimized) | |
| 221 | +| **Redundancy** | High (repeated subproblems) | Avoided (results stored and reused) | |
| 222 | +| **Usability** | Simple but inefficient | Slightly complex but highly efficient | |
| 223 | + |
| 224 | + |
| 225 | +### **Example: Recursive Redundancy** |
| 226 | +To calculate \( F(5) \): |
| 227 | + |
| 228 | +#### Recursive Tree: |
| 229 | +``` |
| 230 | + F(5) |
| 231 | + / \ |
| 232 | + F(4) F(3) |
| 233 | + / \ / \ |
| 234 | + F(3) F(2) F(2) F(1) |
| 235 | + / \ |
| 236 | +F(2) F(1) |
| 237 | +``` |
| 238 | + |
| 239 | +- **Redundancy**: \( F(3) \) is computed **twice**, \( F(2) \) is computed **three times**, and the redundancy increases exponentially as \( n \) grows. |
| 240 | + |
| 241 | + |
| 242 | +### **When Is Recursion Acceptable?** |
| 243 | +The recursive approach can be used for: |
| 244 | +- **Small inputs**: When \( n \) is small (e.g., \( n \leq 10 \)), the inefficiency is negligible. |
| 245 | +- **Educational purposes**: Useful for understanding the problem structure and basic recursion. |
| 246 | +- **Tree-like problems**: Where each recursive call generates unique subproblems without overlap. |
| 247 | + |
| 248 | +### **Conclusion** |
| 249 | +The recursive approach for problems like Fibonacci is not practical for large inputs due to exponential time complexity and stack overflow issues. It is better to use **dynamic programming** with memoization (Top-Down) or tabulation (Bottom-Up) to solve these problems efficiently. |
0 commit comments