101 Logo
onenoughtone

Introduction to Dynamic Programming

What is Dynamic Programming?

Dynamic Programming (DP) is a technique for solving complex problems by breaking them down into simpler subproblems and solving each subproblem only once, storing the solutions to avoid redundant computations.

It's particularly useful for optimization problems where we need to find the best solution among many possible solutions.

Key Characteristics

  • Overlapping Subproblems: The problem can be broken down into subproblems which are reused multiple times
  • Optimal Substructure: The optimal solution to the problem can be constructed from optimal solutions of its subproblems
  • Memoization/Tabulation: Solutions to subproblems are stored to avoid redundant calculations
  • Bottom-up/Top-down: Problems can be solved either way, depending on the approach

When to Use Dynamic Programming

Dynamic Programming is useful when:

  • The problem has overlapping subproblems
  • The problem has optimal substructure
  • You need to find the optimal solution (maximum/minimum value)
  • You need to count the total number of ways to do something
  • A recursive solution is too slow due to repeated calculations

Example: Fibonacci Sequence

Let's look at the classic example of computing the nth Fibonacci number:

// Recursive approach (without DP) function fibonacci(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } // Time Complexity: O(2^n) - exponential // Space Complexity: O(n) - recursion stack // DP approach with memoization (top-down) function fibonacciMemo(n, memo = {}) { if (n in memo) return memo[n]; if (n <= 1) return n; memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo); return memo[n]; } // Time Complexity: O(n) - linear // Space Complexity: O(n) - memo object + recursion stack // DP approach with tabulation (bottom-up) function fibonacciTab(n) { if (n <= 1) return n; const dp = new Array(n + 1); dp[0] = 0; dp[1] = 1; for (let i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } // Time Complexity: O(n) - linear // Space Complexity: O(n) - dp array // DP approach with optimized space (bottom-up) function fibonacciOpt(n) { if (n <= 1) return n; let prev2 = 0; let prev1 = 1; let current; for (let i = 2; i <= n; i++) { current = prev1 + prev2; prev2 = prev1; prev1 = current; } return prev1; } // Time Complexity: O(n) - linear // Space Complexity: O(1) - constant

Memoization vs. Tabulation

Memoization (Top-Down):

  • Starts from the original problem and recursively breaks it down
  • Uses a cache (usually a hash map) to store results of subproblems
  • Computes only the needed subproblems
  • Usually easier to implement
  • May cause stack overflow for large inputs due to recursion

Tabulation (Bottom-Up):

  • Starts from the base cases and builds up to the original problem
  • Uses a table (usually an array) to store results
  • Computes all subproblems in a specific order
  • Usually more efficient in terms of space
  • Avoids recursion and stack overflow issues

Steps to Solve DP Problems

  1. Identify if the problem can be solved using DP (check for overlapping subproblems and optimal substructure)
  2. Define the state: What information do we need to represent a subproblem?
  3. Formulate the recurrence relation: How can we build the solution from smaller subproblems?
  4. Identify the base cases: What are the simplest subproblems we can solve directly?
  5. Decide the approach: Memoization (top-down) or Tabulation (bottom-up)
  6. Implement the solution
  7. Optimize for space if needed
IntroVisualizePatternsPractice
101 Logo
onenoughtone