DP Fibonacci Memoization — Heap Explosion
A Fibonacci memoization job caused OutOfMemoryError after storing millions of entries for large n.
- DP solves problems by storing overlapping subproblem results and reusing them
- Two key properties: optimal substructure and overlapping subproblems
- Two implementation approaches: top-down memoization and bottom-up tabulation
- Memoization adds ~O(1) storage per call; tabulation avoids recursion overhead
- Production insight: exponential explosion happens when subproblems don't actually overlap or state space is too large
- Biggest mistake: trying DP on non-overlapping subproblems — it just adds memory waste
Imagine you're hiking a mountain and at every fork you write down the best path you took so far on a sticky note. Next time you reach that same fork, you don't explore again — you just read the note. Dynamic programming is exactly that: solving a problem once, writing the answer down, and reusing it instead of doing the same work twice. It's the difference between a chef who memorises a recipe after the first attempt and one who starts from scratch every single time.
Every developer hits a wall with certain problems — the recursive solution looks elegant, run it, and it either times out or melts your CPU. Fibonacci up to position 50, shortest paths through a city grid, breaking a string into valid words — these all share a hidden trap: your program is silently solving the same sub-problem hundreds or thousands of times. Dynamic programming (DP) is the algorithmic technique built specifically to demolish that trap, and it's why Google Maps gives you a route in milliseconds instead of minutes.
The core problem DP solves is called 'redundant computation.' Naive recursion is beautifully expressive but brutally wasteful when subproblems overlap — meaning the answer to a smaller version of the problem is needed by multiple larger versions. Without a strategy to store and reuse those answers, your time complexity explodes exponentially. DP converts that exponential work into polynomial work by ensuring each unique subproblem is solved exactly once. That single insight is responsible for making an entire class of problems that felt impossible suddenly become tractable.
By the end of this article you'll understand the two properties that tell you a problem is solvable with DP, know the difference between top-down (memoization) and bottom-up (tabulation) approaches, be able to implement both in Java, and recognise the patterns that appear in real coding interviews. You'll also know the three most common mistakes that trip people up — learn them the easy way.
Here's what's coming: start with the two properties, then walk through memoization and tabulation with concrete code. After that, compare both approaches, cover the repeatable DP patterns you'll see in interviews, and wrap with space optimization and state reduction — the parts that separate a working solution from a production-ready one. No fluff, just the mechanics that senior engineers use daily.
Introduction: What DP Solves and Why It Exists
Rather than a dry definition, let's see DP in action. At its heart, DP is about solving problems by remembering past results. You'll see it used in everything from routing to genetics. The key is recognising two properties: optimal substructure (the optimal solution to the big problem contains optimal solutions to smaller subproblems) and overlapping subproblems (the same subproblems appear multiple times). Without both, DP is the wrong tool.
Here's the thing — most developers first encounter DP with Fibonacci. That's a good start, but it hides a bigger truth: DP isn't about recursion; it's about recognising patterns in computation. If you've ever written a loop that recomputes the same value, you've already felt the pain DP eliminates. The real world is full of these patterns: finding the cheapest flight, compressing video, detecting plagiarism. They all reduce to subproblems that repeat.
In production, the cost of redundant computation isn't just slow code — it's latency spikes that trip your load balancer and trigger pager duty alerts. I've seen a naive recursive implementation of a routing optimizer run for 40 seconds on a 20-city input. After adding memoization, it dropped to 200 milliseconds. That's the difference between a system that scales and one that breaks under load.
- DP trades memory for speed — store once, reuse forever.
- Without overlap, DP is just caching nothing useful.
- Check overlap by counting unique calls vs total calls.
- If overlap ratio > 1, DP can help.
Detecting Optimal Substructure and Overlapping Subproblems
Before writing any DP code, you must confirm the problem has both required properties. Optimal substructure means you can break the problem into smaller pieces and combine their optimal solutions to get the global optimum. Overlapping subproblems means the same subproblem appears in multiple branches. For example, in Fibonacci, fib(5) needs fib(4) and fib(3), and fib(4) also needs fib(3) — that's overlap. In merge sort, subproblems (left and right halves) are disjoint; no overlap, so DP doesn't help. How to check? Draw the recursion tree for a small input. If you see repeated subtrees, you've got overlap.
Here's a trick I've used in code reviews: add a static counter and a map to track how many times each subproblem is called. If the ratio of total calls to unique calls is more than 5, DP will save you at least that factor. If it's close to 1, you're better off with divide and conquer.
One trap: some problems have optimal substructure but no overlap — binary search is a classic. Throwing a memoization cache at binary search adds overhead for zero gain. I've seen this mistake in production URL routing code where the engineer added a cache for path segments, but each path was unique. The cache just consumed memory and caused GC pressure. Always verify overlap first.
- Draw the tree for n=5; you'll see fib(3) appears twice.
- If you memoize, the second call is a cache hit.
- If no repeated nodes (like merge sort), DP is not beneficial.
- Use a debug counter to verify overlap in your own code.
Top-Down Memoization: Recursion with a Cache
Top-down DP starts with the recursive solution, then adds a cache (usually a HashMap or array) to store results of subproblems. When you need a subproblem result, you check the cache first. This approach keeps the recursive structure clear — it's just recursion plus 'if this already computed, return it'. The downside: recursion depth may cause stack overflow for large inputs, and the cache can grow large if many subproblems exist. It's especially good when only some subproblems are needed (sparse dependency).
One thing I've learned the hard way: always use Long for Fibonacci values beyond the 40th term. The int overflow is silent — you get negative numbers and you start blaming the DP logic. Also, computeIfAbsent is not thread-safe. In an interview, mentioning ConcurrentHashMap for multi-threaded environments shows you think about production.
In practice, I've seen teams use memoization on tree-structured data (like parsing expressions) where the dependency graph is sparse and deep. The cache saves massive time, but you need to be careful about the key: if your state has multiple dimensions, use a custom key object or a string concatenation. A Pair class or a nested map works, but be aware of equals/hashCode overhead. In one production system, the cache key was a simple integer index — that's fine for linear problems but breaks for 2D states.
computeIfAbsent is not thread-safe — use ConcurrentHashMap for multi-threaded environments. In single-threaded DP, HashMap is fine.Bottom-Up Tabulation: Iterative and Safe
Bottom-up DP builds a table (usually a 1D or 2D array) starting from the smallest subproblems and working upward. No recursion, no stack overflow, and often easier to reason about space complexity. The order of filling the table must respect dependencies — you compute the smallest subproblems first, then larger ones that depend on them. For Fibonacci, you fill a table of size n+1 from 0..n. The trade-off: you may compute subproblems that are never needed (unlike memoization which computes on demand). For problems like Fibonacci where the whole range is needed, tabulation is ideal.
But don't get complacent — bottom-up can still surprise you. A 2D table of size 1000x1000 with long values takes about 8 MB. That's fine. But a 10^5 x 10^5 table? That's 80 GB — not happening. Always estimate memory before you write the loop.
Here's a practical tip: for many problems, you can reduce the 2D table to 1D by only keeping the previous row. That's the rolling array technique. I'll cover that in the space optimization section, but keep it in mind even here: if you notice your recurrence only looks at dp[i-1][], you can cut memory from O(nm) to O(m). Don't do it prematurely — get the 2D version correct first, then shrink.
- Rows represent decision stages (e.g., items considered, substring indices).
- Columns represent remaining capacity, current value, etc.
- Fill from top-left to bottom-right (or according to dependency direction).
- The final answer is often at dp[n][capacity] or similar.
Choosing Between Memoization and Tabulation
The choice depends on the problem and constraints. Use memoization when: the dependency graph is sparse (many subproblems may never be needed), the problem naturally fits recursion, or you want to prototype quickly from the recursive solution. Use tabulation when: you know exactly which subproblems are needed, recursion depth is a concern, or you need to optimize for space (tabulation can sometimes use O(1) extra space via variable reuse). In many cases, both work equally well — the key is to pick the one that feels natural and meets performance requirements.
Here's my rule of thumb: if the problem says 'find the minimum cost with unlimited items' (unbounded knapsack) and the number of items is small, memoization is fine. If the problem says 'longest common subsequence' and both strings are thousands of characters, go tabulation — the full table is needed anyway.
In interviews, I always start with recursion + memoization. It's easier to explain and debug. Then I mention that if the interviewer wants, I can convert to tabulation. That shows you understand both. In production, I lean tabulation for problems with bounded state space because memory is predictable and you avoid stack issues. But I've also shipped memoization in systems where the state space was huge but only a fraction was ever accessed — the cache kept things fast without blowing up memory (with proper eviction, of course).
Common DP Patterns in Coding Interviews
Most DP interview problems fall into a few patterns: 0/1 Knapsack (take or leave), Unbounded Knapsack (infinite copies), Longest Common Subsequence (LCS), Palindromic Subsequence, Fibonacci-like, Matrix Paths, and Partition Problems. Recognizing the pattern helps you quickly set up the state and recurrence. For example, if you see 'maximise value with weight constraint', you're likely looking at a knapsack DP. If you see 'minimum edit operations', it's Levenshtein (edit distance). Practice these patterns until the recurrence becomes second nature.
The real skill isn't memorising patterns — it's identifying the state. I've seen devs who know the knapsack pattern cold but can't adapt when the constraint is on time instead of weight. State is just the variables you need to uniquely describe a subproblem: usually (index, remaining capacity) or (i, j) for two sequences.
To internalize these patterns, I recommend the following exercise: take a pattern (say, 0/1 knapsack) and rewrite the solution from scratch without looking. Then modify the constraint — change 'weight' to 'time', or 'maximize' to 'count ways'. See how the state and recurrence shift. That's how you build transferable skill.
- Knapsack: choose items with constraints.
- LCS: compare sequences, build table.
- Palindromic: expanding around center or 2D DP.
- Fibonacci: linear recurrence — often O(1) space possible.
Space Optimization in Dynamic Programming
Many DP solutions use a 2D table, but you can often reduce memory to 1D by reusing rows. This matters when n is large (10^5+) and a full table would exceed heap. The key insight: if the recurrence only depends on the previous row (or a few previous states), you can discard older rows. For 0/1 knapsack, you iterate capacity backwards to avoid overwriting. For LIS, you can use a DP array of length n and binary search. Space optimization is not just about memory — it also reduces cache misses and GC pressure. But it makes the code harder to read. Use it only when needed.
A mistake I see often: developers try to space-optimize before they have a correct solution. Get the 2D version working first, then shrink. In interviews, start with full table and then say 'I can optimize this to 1D by reusing rows'. That shows depth without risking correctness.
For production systems, always profile memory before micro-optimizing. I've seen a team spend a day converting a 2D DP to 1D for a problem that ran once per night and consumed 5 MB. That's not the bottleneck. Focus optimization effort where it matters — high-frequency paths or large-scale data pipelines.
State Definition: The Most Critical Step in DP
The single most important decision in any DP solution is defining the state. The state is the set of variables that uniquely identify a subproblem. For Fibonacci, it's just n. For knapsack, it's (index, remaining weight). For LCS, it's (i, j) where i and j are indices into the two strings. Get the state wrong, and your recurrence won't work. Common mistakes: including too many variables (state explosion) or too few (no optimal substructure).
A good heuristic: start with the smallest possible state that captures all decisions made so far. If you can't write the recurrence, you probably need more state. If the DP table is larger than available memory, you probably need less. The art is finding the balance.
Here's a practical approach: when you're stuck, write down everything you think the subproblem needs to know. Then ask, 'Can I compute the next step without this variable?' If yes, remove it. For example, in coin change (unlimited coins), you might think you need (index, amount). But since coins can be reused in any order, the index doesn't matter — just the remaining amount. That's a 1D DP hiding in 2D clothes. Many classic problems have such reductions. The earlier you spot them, the cleaner your solution.
- If you need to remember which items you've used, that's part of the state.
- If the future depends only on capacity (like unbounded knapsack), state can just be (remaining weight).
- If you need to decide pick/no-pick for each item, state must include index.
- State reduction is the key to efficient DP.
State Reduction: When Your DP Table Is Too Big
You've identified the problem, defined the state, and started coding. Then you realise the DP table would need 10^8 entries — that's 800 MB for longs. Production won't allow it. The fix is state reduction: eliminating dimensions that aren't strictly necessary.
Most DP states are larger than needed because we include variables that the recurrence doesn't actually need to remember. For example, in the coin change problem with unlimited coins, the state can be just the remaining amount — you don't need the index because the order of coins doesn't matter. That's a 1D DP hiding inside a 2D structure.
Here's the trick I've seen senior engineers use: write down every variable you think you need, then ask 'does the future decision depend on this variable?' If the answer is no, remove it. Every dimension you drop saves an order of magnitude in memory.
Another powerful technique: if your recurrence only depends on the previous row (or k previous rows), you can use a rolling array. That's not state reduction in the dimensionality sense, but it reduces memory footprint significantly. For edit distance (LCS), you only need two rows (or even one with careful updates). For knapsack, the rolling array is essentially the 1D version we already discussed.
Be careful: don't reduce state at the cost of correctness. The most common mistake is removing a dimension that the recurrence actually needs, then getting wrong answers for edge cases. Always test with small examples before scaling up.
- If the recurrence never looks back more than one row, drop earlier rows.
- If the order of decisions doesn't matter, drop the index.
- If a variable is always the same across all subproblems, it's not a state variable.
- One dimension saved = one power of ten in memory.
Production DP: Cache Eviction, Input Bounding, and Thread Safety
You've got memoization working in your dev environment. Then you deploy to production and the JVM runs out of memory. The cache grows unbounded because the input space is larger than expected. This is the most common DP failure in production systems. The fix isn't in the recursion logic — it's in how you manage the cache.
First, always bound the input space. Validate that n is within a reasonable range before recursing. For Fibonacci, that's easy. For graph-based DP, define the maximum state count upfront.
Second, use an eviction policy. A LinkedHashMap with LRU order limits memory to a fixed size. But be careful: LRU may evict commonly needed states if the access pattern is non-local. For problems with repetitive access to a small working set, LRU works. For uniform random access, consider a fixed-size array.
Third, thread safety. If your memoization cache is accessed by multiple threads, HashMap will corrupt. Use ConcurrentHashMap or synchronize access. The performance cost is usually acceptable compared to recomputation.
In production, also monitor cache hit rate and size. A sudden drop in hit rate may indicate a change in input distribution. I've seen a system where a new data source caused cache thrashing — the hit rate dropped from 90% to 5%, and response times increased 10x. The fix: tune the cache size and eviction policy based on the new workload.
Don't assume that DP caching is free. Every cache entry takes memory and GC time. For high-throughput systems, even a small overhead multiplied by millions of requests becomes significant. Profile before and after adding caching.
Fibonacci With Memoization That Still Crashed the Server
computeIfAbsent call triggered recursive calls for huge n, growing the map beyond heap.- DP memoization is not a silver bullet — you must bound the input space and cache size.
- Bottom-up tabulation often avoids stack overflow and large cache overhead.
- Always validate inputs before applying DP, especially when recursion depth could be extreme.
size() > 10000; } });Key takeaways
Common mistakes to avoid
7 patternsMemorising syntax before understanding the concept
Skipping practice and only reading theory
Using recursion without memoization and expecting it to scale
Keeping unbounded cache without eviction in production
Ignoring space optimization until it's too late
Defining state incorrectly - either too many or too few variables
Using recursion for DP in production without setting a stack limit
Interview Questions on This Topic
What are the two key properties a problem must have for dynamic programming to be applicable?
Frequently Asked Questions
That's Dynamic Programming. Mark it forged?
12 min read · try the examples if you haven't