Learning dynamic programming from scratch with recursion.

Recursion With a Notebook Is Dynamic Programming

I remember sitting in a windowless grad student lounge at 3:00 AM, staring at a whiteboard covered in recursive calls that felt more like a fever dream than actual logic. Everyone around me was treating dynamic programming like some mystical ritual—memorize the recurrence relation, plug in the base cases, and pray the complexity doesn’t explode. It’s a frustrating way to learn, and frankly, it’s why so many people hit a wall when they try to implement dynamic programming from scratch in a real-world system. If you’re just memorizing patterns without seeing how the state space actually unfolds, you aren’t learning an algorithm; you’re just performing a trick that will fail the moment the constraints change.

I’m not interested in giving you a collection of templates to copy and paste into your LeetCode practice. Instead, I want to walk you through the actual mechanics of how we break a problem down into overlapping subproblems and, more importantly, how we decide whether to build up from the bottom or dive in from the top. My goal is to help you develop an intuition for the state transition, so that when you sit down to write code, you aren’t guessing. We are going to build this from the ground up, focusing on the “why” behind the memoization, because that is the only way to ensure the logic actually holds.

Table of Contents

Identifying the Optimal Substructure Property and Overlapping Subproblems

Identifying the Optimal Substructure Property and Overlapping Subproblems

Before you even touch a keyboard, you have to look for two specific signals in your problem. The first is the optimal substructure property. This isn’t just fancy terminology; it means that the solution to your main problem is actually built from the solutions to smaller versions of itself. If solving a sub-problem doesn’t actually help you solve the larger one, you aren’t looking at a dynamic programming candidate—you’re just looking at a standard recursive mess. I once spent a whole afternoon trying to force a greedy approach onto a problem that lacked this property, only to realize the local “best” choice was leading me into a dead end.

The second signal is the presence of overlapping subproblems. This is where the real efficiency gains live. In a naive recursive tree, you’ll often find yourself calculating the exact same state over and over again. It’s incredibly wasteful. When you see these redundant branches, you know you can stop recalculating and start storing. Whether you eventually choose a top-down vs bottom-up approach depends on how you want to manage your memory, but the core goal remains the same: stop doing the same work twice.

Navigating the Choice Between Top Down vs Bottom Up Approach

Once you’ve identified that your problem actually possesses the necessary properties, you face a fork in the road: how do you actually build the solution? In practice, this usually comes down to the choice between a top-down vs bottom-up approach. The top-down method feels more intuitive to me because it mirrors how we naturally think about breaking a large problem into smaller pieces. You start with the main goal and use recursion with memoization to store results as you encounter them. It’s elegant, but you have to be careful; if your recursion depth gets too high, you’ll hit a stack overflow, which is a physical limitation of the system that no amount of cleverness can bypass.

The bottom-up alternative, often called the tabulation method, takes a more mechanical route. Instead of starting at the top, you solve the smallest possible subproblems first and use those results to build a table, filling it up piece by piece until you reach your target. I often prefer this when I’m performing a rigorous space and time complexity analysis because it’s easier to see exactly how much memory you’re consuming. There’s no hidden recursion overhead, just a predictable, iterative climb toward the solution.

Five Ways to Stop Guessing and Start Building

  • Stop trying to write the code immediately. I have seen too many people dive into a `for` loop before they have even proven that a subproblem actually exists. You need to sketch the recurrence relation on paper first; if you cannot express the relationship between $F(n)$ and $F(n-1)$ in plain mathematical terms, no amount of clever coding will save you from a logic error.
  • Trace a single, tiny case by hand. When I’m debugging a complex system, I don’t look at the whole architecture; I look at the smallest unit of failure. Do the same with DP. If your algorithm fails for $n=4$, don’t bother checking $n=100$. Manually step through the state transitions for a tiny input to ensure your “optimal” choice at step two isn’t actually poisoning the well for step four.
  • Respect the state space, but don’t be blinded by it. It is easy to get lost in a multidimensional array, but always ask yourself: “What is the absolute minimum amount of information I need to carry forward to make the next decision?” If you are carrying variables in your state that don’t actually affect future outcomes, you aren’t just being inefficient—you’re making the problem harder than it needs to be.
  • Understand the “Why” of Memoization versus Tabulation. People often treat them as interchangeable synonyms, but they aren’t. Top-down memoization is intuitive because it follows the natural recursion of the problem, but it carries the overhead of the call stack. Bottom-up tabulation is often more performant and allows for space optimization, but it requires you to strictly define the order of computation. Choose the one that matches your mental model of the problem’s progression.
  • Watch out for the “Greedy Trap.” This is where most students stumble. Just because a choice looks optimal right now doesn’t mean it’s part of the global optimum. If you find yourself thinking, “Well, this is clearly the best move,” pause. If that move prevents you from accessing a much better state later, you aren’t doing dynamic programming; you’re just being greedy. DP is fundamentally about accounting for the consequences of your current choices on the future.

The Core Mechanics to Carry Forward

Stop looking for a “template” to plug your problem into. Dynamic programming isn’t a specific coding pattern; it is a way of recognizing that a large, daunting problem is actually just a collection of smaller, identical problems that keep bumping into each other. If you can’t see the overlap, you aren’t doing DP; you’re just doing recursion.

The choice between top-down and bottom-up is a trade-off between intuition and efficiency. Memoization (top-down) is often easier to reason about because it follows the natural logic of the problem, but the iterative bottom-up approach is usually more robust in production because you aren’t risking a stack overflow by building a massive recursion tree.

Always verify the “state” before you write a single line of code. If your state definition is too thin, you won’t have enough information to make a decision; if it’s too bloated, your complexity will explode and the algorithm will become useless. The goal is to find the absolute minimum amount of information needed to solve the next subproblem.

Beyond the Recurrence Relation

We have moved past the stage of simply memorizing templates. We’ve looked at how to spot the optimal substructure that allows a large problem to be broken down, and we’ve weighed the trade-offs between the intuitive elegance of top-down memoization and the raw efficiency of bottom-up tabulation. It is easy to get lost in the syntax of a recursive function, but the real work happens in the mental model you build of how those subproblems overlap. If you can see the interconnectedness of the state space, the implementation becomes a mere formality rather than a guessing game.

My advice is to resist the urge to jump straight to the “optimized” solution. When I am working on a new system or even just tinkering with an old mechanical calculator, I find that the most elegant results only emerge after I have wrestled with the most unrefined, brute-force version of the logic. Dynamic programming isn’t a magic trick; it is a disciplined way of organizing computation to avoid redundant labor. Don’t just aim for the code that passes the test cases—aim for the code where you can trace every single state transition back to its fundamental mechanism.

About Dr. Ingrid Falk-Weller

I write for the person who wants to understand the mechanism, not memorise the conclusion. If a claim has a caveat, the caveat goes in the paragraph, not a footnote.