101 Logo
onenoughtone

Memoization Pattern

What is Memoization?

Memoization is a top-down dynamic programming technique where we solve the problem recursively and store the results of subproblems to avoid redundant calculations.

It's essentially an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again.

When to Use Memoization

Memoization is useful when:

  • The problem has a natural recursive structure
  • The recursive solution has overlapping subproblems
  • You prefer a top-down approach that follows the natural recursive structure
  • You only need to compute a subset of all possible states

Memoization Template

  1. Write a recursive function that solves the problem
  2. Add a memo object/array as a parameter to store results
  3. Before computing anything, check if the result for the current state is already in the memo
  4. After computing the result, store it in the memo before returning

Example: Fibonacci

Here's a memoized solution for computing the nth Fibonacci number:

function fibonacci(n, memo = {}) { // Check if we've already computed this value if (n in memo) return memo[n]; // Base cases if (n <= 1) return n; // Recursive case with memoization memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo); return memo[n]; } // Example usage: console.log(fibonacci(10)); // Output: 55 console.log(fibonacci(40)); // Output: 102334155 (would be very slow without memoization)

Example: Longest Common Subsequence

Here's a memoized solution for finding the longest common subsequence of two strings:

function longestCommonSubsequence(text1, text2, i = 0, j = 0, memo = {}) { // Create a key for the current state const key = i + ',' + j; // Check if we've already computed this state if (key in memo) return memo[key]; // Base case: if we've reached the end of either string if (i === text1.length || j === text2.length) return 0; // If the current characters match if (text1[i] === text2[j]) { memo[key] = 1 + longestCommonSubsequence(text1, text2, i + 1, j + 1, memo); } else { // If they don't match, take the maximum of two possibilities memo[key] = Math.max( longestCommonSubsequence(text1, text2, i + 1, j, memo), longestCommonSubsequence(text1, text2, i, j + 1, memo) ); } return memo[key]; } // Example usage: console.log(longestCommonSubsequence("abcde", "ace")); // Output: 3 (LCS is "ace") console.log(longestCommonSubsequence("abc", "abc")); // Output: 3 (LCS is "abc") console.log(longestCommonSubsequence("abc", "def")); // Output: 0 (no common subsequence)

Example: Coin Change

Here's a memoized solution for the coin change problem:

function coinChange(coins, amount, index = 0, memo = {}) { // Create a key for the current state const key = amount + ',' + index; // Check if we've already computed this state if (key in memo) return memo[key]; // Base cases if (amount === 0) return 0; if (amount < 0 || index === coins.length) return Infinity; // Two options: include the current coin or skip it const includeCoin = 1 + coinChange(coins, amount - coins[index], index, memo); const skipCoin = coinChange(coins, amount, index + 1, memo); // Store the minimum in memo and return memo[key] = Math.min(includeCoin, skipCoin); return memo[key]; } // Wrapper function to handle the result function minCoins(coins, amount) { const result = coinChange(coins, amount); return result === Infinity ? -1 : result; } // Example usage: console.log(minCoins([1, 2, 5], 11)); // Output: 3 (5 + 5 + 1) console.log(minCoins([2], 3)); // Output: -1 (not possible) console.log(minCoins([1, 2, 5], 0)); // Output: 0 (no coins needed)

Pros and Cons of Memoization

Pros:

  • Easy to implement from a recursive solution
  • Computes only the needed states
  • Follows the natural recursive structure of the problem

Cons:

  • May cause stack overflow for large inputs due to recursion
  • Slightly more overhead due to recursive function calls
  • May be less intuitive for problems that are naturally iterative
IntroVisualizePatternsPractice
101 Logo
onenoughtone