Array of silver Bitcoin coins arranged in a regular pattern over a light background.

Dynamic Programming Explained with Fibonacci and Knapsack

Dynamic programming optimizes recursive solutions. Walk through memoization and tabulation using two classic problems in Python.

Dynamic programming is a technique often used to improve the efficiency of recursive algorithms. Many problems that involve breaking down a large task into smaller overlapping subproblems can benefit from this approach. Instead of recomputing the same subproblem multiple times, dynamic programming stores previously computed results and reuses them when needed. This can transform algorithms with exponential time complexity into polynomial or linear ones, depending on the problem structure.

Two classic examples that illustrate the core ideas of dynamic programming are the Fibonacci sequence and the 0/1 knapsack problem. Both exhibit overlapping subproblems and optimal substructure, making them ideal candidates for optimization. The Fibonacci sequence, while simple, demonstrates how naive recursion leads to repeated work, and how either memoization or tabulation can eliminate that redundancy. The knapsack problem introduces a more complex decision-making process, where the goal is to maximize value under a weight constraint, and dynamic programming provides a systematic way to explore combinations without exhaustive enumeration.

This article walks through the reasoning behind dynamic programming, explains the two main implementation strategies — memoization (top-down) and tabulation (bottom-up) — and applies them to these two well-known problems. The focus is on understanding the methodology and the trade-offs between different approaches, rather than on writing specific code snippets.

Understanding Overlapping Subproblems and Optimal Substructure

Before applying dynamic programming, it is helpful to identify whether a problem contains overlapping subproblems and optimal substructure. Overlapping subproblems occur when the same smaller instances of a problem are solved repeatedly during the execution of a recursive algorithm. In a naive recursive implementation of the Fibonacci sequence, for example, F(5) calls F(4) and F(3), but F(4) also calls F(3) and F(2), so F(3) is computed multiple times. This redundancy grows exponentially if not addressed.

Optimal substructure means that an optimal solution to a larger problem can be constructed from optimal solutions to its subproblems. In the case of the knapsack problem, the decision of whether to include or exclude an item depends on the best possible value achievable with the remaining capacity. This property allows a problem to be solved by building up solutions from smaller instances. Recognizing these two characteristics is the first step in deciding whether dynamic programming is a suitable technique.

Once these characteristics are identified, the next step is to choose between a top-down approach, which starts with the original problem and recursively breaks it down while caching results, and a bottom-up approach, which solves all subproblems in a predefined order and combines their results to form the final answer. Both methods ultimately compute the same values, but they differ in implementation style and memory usage.

Memoization: Top-Down Dynamic Programming

Memoization is a technique that extends a recursive algorithm by storing the results of subproblems in a data structure, such as a dictionary or an array. When the algorithm encounters a subproblem that has already been solved, it retrieves the stored result instead of recomputing it. This approach retains the natural recursive structure of the problem while eliminating redundant calculations.

For the Fibonacci sequence, a memoized recursive function computes F(n) by first checking whether the result for n is already stored. If it is, the stored value is returned immediately. Otherwise, the function recursively computes F(n-1) and F(n-2), stores the result, and returns it. This reduces the time complexity from exponential to linear, as each Fibonacci number from 0 to n is computed exactly once. The space complexity remains linear due to the recursion stack and the storage array.

In the 0/1 knapsack problem, a memoized solution considers each item and the remaining capacity. The recursive function explores two possibilities: including the current item (if it fits) or skipping it. The maximum value for a given state (index, remaining capacity) is cached. This avoids recomputing the same combination of item and capacity multiple times. While the recursion depth equals the number of items, the number of unique states is bounded by the product of the item count and the capacity, leading to a pseudopolynomial time complexity that is far more efficient than brute force.

Tabulation: Bottom-Up Dynamic Programming

Tabulation takes a different route by solving all subproblems iteratively, starting from the smallest instances and building upward. The results are stored in a table (usually an array or matrix) that is filled in a systematic order. This approach avoids recursion altogether and often uses less memory because it can be designed without a call stack.

For the Fibonacci sequence, tabulation involves initializing an array of size n+1, setting F(0)=0 and F(1)=1, and then iteratively computing each subsequent value as the sum of the two previous ones. This yields a linear time and constant space (if only the last two values are kept) solution. The bottom-up method is straightforward and does not require any conditional caching logic.

In the knapsack problem, a tabulation approach uses a two-dimensional table where rows represent items (or item indices) and columns represent capacity values from 0 to W. Each cell (i, w) stores the maximum value achievable using the first i items with total weight not exceeding w. The table is filled by considering whether adding item i (with weight w_i and value v_i) improves the best value already known for the same capacity without that item. Iterating over all items and all capacities results in O(n * W) time and space complexity. The final answer is found in the bottom-right cell of the table.

Comparing Memoization and Tabulation

Both memoization and tabulation achieve the same theoretical time complexity for a given problem, but they have practical differences. Memoization is often easier to implement because it follows the natural recursive formulation of the problem. It also only computes subproblems that are actually needed, which can be beneficial when the state space is large but many states are unreachable. However, the recursion overhead and the risk of stack overflow for deep recursion can be drawbacks.

Tabulation, on the other hand, is typically faster in practice due to the absence of recursion overhead and the ability to use tight loops. It also provides more predictable memory usage. A potential downside is that it may compute many subproblems that are not strictly required for the final solution, especially if the problem has a sparse state space. Choosing between the two approaches often depends on the specific constraints of the problem, such as input size, recursion depth limitations, and whether all subproblems are likely to be needed.

For the Fibonacci sequence, tabulation is almost always preferred due to its simplicity and low memory footprint. For the knapsack problem, both approaches are commonly used, but tabulation has the advantage of avoiding deep recursion and being easier to optimize for space by using a one-dimensional array. The memoized version, however, can be more intuitive for those who are comfortable with recursion.

Applying Dynamic Programming to Other Problems

The principles demonstrated with Fibonacci and the knapsack problem extend to many other domains, including sequence alignment, shortest paths, and resource allocation. When encountering a new problem, the first step is to look for overlapping subproblems and optimal substructure. Once these are identified, the next step is to define the state and the recurrence relation. The state typically captures the parameters that describe a subproblem, such as an index, a remaining capacity, or a position in a string. The recurrence relation expresses how the optimal value for a state is derived from optimal values of smaller states.

After defining the state and recurrence, either memoization or tabulation can be used to implement the solution. It is often useful to start with a memoized recursive version to verify correctness, then convert to tabulation for improved performance if needed. The iterative table-filling process can also reveal opportunities for further optimization, such as reducing the dimensionality of the table or using rolling arrays.

Dynamic programming is not a one-size-fits-all technique, but its systematic approach to reusing subproblem results makes it a powerful tool in algorithm design. By understanding the fundamental ideas through concrete examples like Fibonacci and knapsack, one can build a solid foundation for tackling more complex optimization problems.

Get programming insights delivered to your inbox

Each edition includes practical articles on programming languages, algorithms, and development best practices. Content is suitable for developers at any skill level.

Stay up to date with the latest news

We use cookies

We use cookies to ensure the proper functioning of the website, analyze traffic, and improve your experience. You can accept all cookies or reject them — the site will continue to operate. For more details, read our Cookie Policy.