💡 Memoization avoids recomputing subproblems by caching results, drastically improving efficiency over brute force.
Intuition
Store results of recursive calls in a table keyed by coin index and remaining amount to reuse them.
Algorithm
- Initialize a memo table to store results for each (index, amount) pair.
- If amount is 0, return 1.
- If amount is negative or no coins left, return 0.
- If result is cached, return it.
- Otherwise, compute ways by excluding and including current coin, cache and return.
💡 Memoization turns exponential recursion into polynomial by pruning duplicate calls.
Recurrence:dp[i][amt] = dp[i+1][amt] + dp[i][amt - coins[i]]
def change(amount, coins):
memo = {}
def dfs(i, amt):
if amt == 0:
return 1
if amt < 0 or i == len(coins):
return 0
if (i, amt) in memo:
return memo[(i, amt)]
memo[(i, amt)] = dfs(i + 1, amt) + dfs(i, amt - coins[i])
return memo[(i, amt)]
return dfs(0, amount)
if __name__ == '__main__':
print(change(5, [1, 2, 5])) # Output: 4
Line Notes
memo = {}Cache to store computed results for subproblems
if (i, amt) in memo:Return cached result to avoid recomputation
memo[(i, amt)] = dfs(i + 1, amt) + dfs(i, amt - coins[i])Store computed ways including and excluding coin
return memo[(i, amt)]Return cached or newly computed result
import java.util.*;
public class Solution {
private Map<String, Integer> memo = new HashMap<>();
public int change(int amount, int[] coins) {
return dfs(coins, 0, amount);
}
private int dfs(int[] coins, int i, int amt) {
if (amt == 0) return 1;
if (amt < 0 || i == coins.length) return 0;
String key = i + "," + amt;
if (memo.containsKey(key)) return memo.get(key);
int res = dfs(coins, i + 1, amt) + dfs(coins, i, amt - coins[i]);
memo.put(key, res);
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.change(5, new int[]{1, 2, 5})); // Output: 4
}
}
Line Notes
private Map<String, Integer> memo = new HashMap<>();Memo cache keyed by coin index and amount
String key = i + "," + amt;Create unique key for current subproblem
if (memo.containsKey(key)) return memo.get(key);Return cached result if available
memo.put(key, res);Store computed result to avoid recomputation
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
unordered_map<string, int> memo;
public:
int change(int amount, vector<int>& coins) {
return dfs(coins, 0, amount);
}
private:
int dfs(vector<int>& coins, int i, int amt) {
if (amt == 0) return 1;
if (amt < 0 || i == coins.size()) return 0;
string key = to_string(i) + "," + to_string(amt);
if (memo.count(key)) return memo[key];
memo[key] = dfs(coins, i + 1, amt) + dfs(coins, i, amt - coins[i]);
return memo[key];
}
};
int main() {
Solution sol;
vector<int> coins = {1, 2, 5};
cout << sol.change(5, coins) << endl; // Output: 4
return 0;
}
Line Notes
unordered_map<string, int> memo;Cache for storing subproblem results
string key = to_string(i) + "," + to_string(amt);Unique key for memoization
if (memo.count(key)) return memo[key];Return cached result if present
memo[key] = dfs(coins, i + 1, amt) + dfs(coins, i, amt - coins[i]);Store computed ways
function change(amount, coins) {
const memo = new Map();
function dfs(i, amt) {
if (amt === 0) return 1;
if (amt < 0 || i === coins.length) return 0;
const key = i + ',' + amt;
if (memo.has(key)) return memo.get(key);
const res = dfs(i + 1, amt) + dfs(i, amt - coins[i]);
memo.set(key, res);
return res;
}
return dfs(0, amount);
}
console.log(change(5, [1, 2, 5])); // Output: 4
Line Notes
const memo = new Map();Memo cache to store subproblem results
const key = i + ',' + amt;Unique key for current subproblem
if (memo.has(key)) return memo.get(key);Return cached result if available
memo.set(key, res);Cache computed result for reuse
TimeO(n * amount) where n is number of coins
SpaceO(n * amount) for memo table and recursion stack
Memoization ensures each subproblem computed once; total subproblems = n*amount
💡 For amount=20 and 3 coins, at most 60 subproblems computed, much faster than brute force.
Interview Verdict: Accepted
This approach is efficient enough for most inputs and a good first optimization.