The Algorithms - Python Python TheAlgorithms/Python

Dynamic Programming in TheAlgorithms/Python

Seven DP patterns from memoized sequences to all-pairs shortest paths.

7 stops ~14 min Verified 2026-05-05
What you will learn
  • How memoization in the Fibonacci class avoids redundant sequence computation
  • How the 0/1 knapsack DP table encodes include-or-skip decisions at each item and capacity
  • How LCS fills a 2D table by matching characters and traces back the subsequence
  • How bottom-up edit distance builds costs from empty-string base cases
  • How matrix chain order uses interval DP to find the cheapest multiplication sequence
  • How the recursive LIS algorithm handles decreasing prefixes by branching on the pivot
  • How Floyd-Warshall relaxes all vertex pairs through every intermediate vertex in three nested loops
Prerequisites
  • Familiarity with Python classes and list comprehensions
  • Basic understanding of recursion and memoization concepts
  • Comfort reading nested for-loops and 2D array indexing
1 / 7

Fibonacci DP: a class that grows its cache on demand

dynamic_programming/fibonacci.py:7

Memoization stores prior results so that each Fibonacci number is computed at most once regardless of how many times it is requested.

The naive recursive Fibonacci recomputes every subproblem from scratch, giving exponential time. This class avoids that by storing the sequence in self.sequence and extending it lazily. The walrus operator on line 21 computes how many new values are needed: difference = index - (len(self.sequence) - 2). If that is positive, the loop appends exactly difference new values, each computed from the last two. If the same or a larger index was already requested, the existing cache is returned without any computation. The doctest confirms that get(10) returns the first ten Fibonacci numbers from the cached list. This is memoization through incremental accumulation rather than a dictionary lookup, which works because Fibonacci values are always needed in order. For a deeper look including the Binet formula and matrix approaches in the same file, see the deeper tour.

Key takeaway

The Fibonacci class grows its sequence list only as far as needed, so any repeated or sequential requests share the same computed values.

class Fibonacci:
    def __init__(self) -> None:
        self.sequence = [0, 1]

    def get(self, index: int) -> list:
        """
        Get the Fibonacci number of `index`. If the number does not exist,
        calculate all missing numbers leading up to the number of `index`.

        >>> Fibonacci().get(10)
        [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
        >>> Fibonacci().get(5)
        [0, 1, 1, 2, 3]
        """
        if (difference := index - (len(self.sequence) - 2)) >= 1:
            for _ in range(difference):
                self.sequence.append(self.sequence[-1] + self.sequence[-2])
        return self.sequence[:index]
2 / 7

0/1 Knapsack: a 2D table encodes include-or-skip at each item and capacity

dynamic_programming/knapsack.py:29

The knapsack DP table encodes whether each item is included by choosing between the value of including it and the value of skipping it.

The 0/1 knapsack problem asks which items to include to maximize value without exceeding a weight limit. The brute-force approach checks every subset in exponential time. The DP approach recognizes that the decision for item i depends only on the best outcome for the first i-1 items, which is already in the table. If item i fits at capacity w_, the algorithm takes the maximum of two options: include the item (its value plus the best solution at the remaining capacity) or skip it (the best solution for the same capacity without this item). If the item does not fit, it is skipped and the prior row's value is copied. The table grows bottom-up from zero items to all n items, filling each cell in constant time. The answer is at dp[n][w]. For the full file including a solution-extraction function, see the deeper tour.

Key takeaway

Each cell dp[i][w] encodes one binary decision: include item i or skip it, taking the maximum of both options.

def knapsack(w, wt, val, n):
    dp = [[0] * (w + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w_ in range(1, w + 1):
            if wt[i - 1] <= w_:
                dp[i][w_] = max(val[i - 1] + dp[i - 1][w_ - wt[i - 1]], dp[i - 1][w_])
            else:
                dp[i][w_] = dp[i - 1][w_]

    return dp[n][w_], dp
3 / 7

LCS: fill the table by matching characters, trace back the sequence

dynamic_programming/longest_common_subsequence.py:59

LCS builds a 2D DP table where each cell holds the length of the longest common subsequence of the two string prefixes ending at that cell.

The LCS recurrence says: if the current characters match, the LCS of these two prefixes is one longer than the LCS of the shorter prefixes; if they do not match, take the longer of removing one character from either string. The single-line recurrence on line 65 captures all three cases in a max call. After the table is filled, dp[m][n] holds the length of the LCS. To recover the actual subsequence, the algorithm walks backward from dp[m][n]: when the diagonal value plus the match indicator equals the current cell, a matched character is prepended to seq and both indices step back; otherwise the algorithm steps toward the larger neighbor. The doctest verifies that the LCS of "programming" and "gaming" is "gaming" with length 6. For the full module, see the deeper tour.

Key takeaway

LCS fills the table forward in O(mn) and recovers the sequence backward in O(m+n) by tracing which cells were reached via diagonal vs. vertical vs. horizontal moves.

    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            match = 1 if x[i - 1] == y[j - 1] else 0

            dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] + match)

    seq = ""
    i, j = m, n
    while i > 0 and j > 0:
        match = 1 if x[i - 1] == y[j - 1] else 0

        if dp[i][j] == dp[i - 1][j - 1] + match:
            if match == 1:
                seq = x[i - 1] + seq
            i -= 1
            j -= 1
        elif dp[i][j] == dp[i - 1][j]:
            i -= 1
        else:
            j -= 1

    return dp[m][n], seq
4 / 7

Edit distance: base cases from empty strings, three operations per cell

dynamic_programming/edit_distance.py:59

Edit distance builds a table where each cell holds the minimum number of insert, delete, or substitute operations to align two string prefixes.

Edit distance asks: what is the minimum number of single-character operations needed to transform one string into another? The base cases anchor the table: an empty first string requires j insertions to become a string of length j, and an empty second string requires i deletions. For matching characters, no operation is needed and the diagonal value propagates unchanged. For mismatching characters, the algorithm considers all three operations: delete a character from the first string (look up), insert a character into the first string (look left), or substitute (look diagonally). Adding 1 to the minimum of those three previous cells gives the cost for the current pair. The doctest confirms that transforming "intention" to "execution" takes 5 operations. For the top-down memoized version in the same file, see the deeper tour.

Key takeaway

Edit distance fills three boundary conditions first, then each interior cell takes 1 plus the minimum of its three neighbors representing the three edit operations.

    def min_dist_bottom_up(self, word1: str, word2: str) -> int:
        """
        >>> EditDistance().min_dist_bottom_up("intention", "execution")
        5
        >>> EditDistance().min_dist_bottom_up("intention", "")
        9
        >>> EditDistance().min_dist_bottom_up("", "")
        0
        """
        self.word1 = word1
        self.word2 = word2
        m = len(word1)
        n = len(word2)
        self.dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]

        for i in range(m + 1):
            for j in range(n + 1):
                if i == 0:  # first string is empty
                    self.dp[i][j] = j
                elif j == 0:  # second string is empty
                    self.dp[i][j] = i
                elif word1[i - 1] == word2[j - 1]:  # last characters are equal
                    self.dp[i][j] = self.dp[i - 1][j - 1]
                else:
                    insert = self.dp[i][j - 1]
                    delete = self.dp[i - 1][j]
                    replace = self.dp[i - 1][j - 1]
                    self.dp[i][j] = 1 + min(insert, delete, replace)
        return self.dp[m][n]
5 / 7

Matrix chain order: interval DP finds the cheapest multiplication sequence

dynamic_programming/matrix_chain_order.py:13

Matrix chain DP fills a triangular table by trying every split point for chains of increasing length, storing the cheapest scalar multiplication count.

Multiplying a chain of matrices in different orders produces the same result but different numbers of scalar multiplications. For three matrices of dimensions 10x30, 30x5, and 5x20, one ordering requires 1500 + 1000 = 2500 multiplications and another requires 3000 + 6000 = 9000. The algorithm finds the optimal parenthesization without trying every possibility. It fills the matrix table by chain length: single matrices have zero cost; chains of two matrices have one split point; longer chains try every split point from a to b-1. For each split at index c, the cost is the left sub-chain cost plus the right sub-chain cost plus the cost of multiplying the two resulting matrices together. The sol table records the best split point at each interval for parenthesization reconstruction. The doctest shows that a 10x30 times 30x5 chain costs 1500 scalar multiplications.

Key takeaway

Matrix chain DP is interval DP: it builds costs for short chains first, then combines them to find the optimal split for longer chains in O(n cubed) time.

def matrix_chain_order(array: list[int]) -> tuple[list[list[int]], list[list[int]]]:
    """
    >>> matrix_chain_order([10, 30, 5])
    ([[0, 0, 0], [0, 0, 1500], [0, 0, 0]], [[0, 0, 0], [0, 0, 1], [0, 0, 0]])
    """
    n = len(array)
    matrix = [[0 for _ in range(n)] for _ in range(n)]
    sol = [[0 for _ in range(n)] for _ in range(n)]

    for chain_length in range(2, n):
        for a in range(1, n - chain_length + 1):
            b = a + chain_length - 1

            matrix[a][b] = sys.maxsize
            for c in range(a, b):
                cost = (
                    matrix[a][c] + matrix[c + 1][b] + array[a - 1] * array[c] * array[b]
                )
                if cost < matrix[a][b]:
                    matrix[a][b] = cost
                    sol[a][b] = c
    return matrix, sol
6 / 7

LIS: a recursive approach that branches on the pivot

dynamic_programming/longest_increasing_subsequence.py:19

The recursive LIS function splits the array at the pivot and compares the increasing prefix with the longest subsequence of elements smaller than the pivot.

The standard iterative LIS algorithm uses patience sorting or binary search and achieves O(n log n) time. This implementation uses recursion to expose the structure of the problem directly. The pivot is always array[0]. The function searches rightward for the first element smaller than the pivot. When found, it recurses on that suffix to get the best subsequence starting from there. It then builds a second candidate: keep the pivot and recurse on all elements that are greater than or equal to the pivot, constructing the longest non-decreasing run starting at the pivot. Whichever candidate is longer is returned. The doctests show edge cases including a decreasing array where only the last two elements form an increasing run, and a flat array where all equal elements count. The recursive structure is exponential in the worst case, making this an educational rather than production implementation.

Key takeaway

The recursive LIS branches at each pivot: one path finds the best subsequence ignoring the pivot, another builds the longest run starting with it.

def longest_subsequence(array: list[int]) -> list[int]:  # This function is recursive
    """
    Some examples

    >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80])
    [10, 22, 33, 41, 60, 80]
    >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9])
    [1, 2, 3, 9]
    >>> longest_subsequence([28, 26, 12, 23, 35, 39])
    [12, 23, 35, 39]
    >>> longest_subsequence([9, 8, 7, 6, 5, 7])
    [5, 7]
    >>> longest_subsequence([1, 1, 1])
    [1, 1, 1]
    >>> longest_subsequence([])
    []
    """
    array_length = len(array)
    # If the array contains only one element, we return it (it's the stop condition of
    # recursion)
    if array_length <= 1:
        return array
        # Else
    pivot = array[0]
7 / 7

Floyd-Warshall: three nested loops relax all vertex pairs

dynamic_programming/floyd_warshall.py:26

Floyd-Warshall considers every vertex as a potential intermediate hop and updates pairwise distances when routing through that vertex is cheaper.

Dijkstra finds the shortest paths from a single source. Floyd-Warshall solves a harder problem: all-pairs shortest paths in one pass. The algorithm's correctness follows from a simple recurrence: the shortest path from i to j either does not use vertex k, or it goes through k and the two sub-paths i to k and k to j are themselves shortest paths. Iterating k from 0 to n-1 considers each vertex as a potential intermediate in order, and by the time all k values have been processed, every valid intermediate has been considered for every pair. The three-line body of the loops is the entire algorithm. The doctest confirms that after adding edges 0-1 and 1-2, the shortest path from 0 to 2 is 3 (through vertex 1), while 2 to 0 remains inf because the graph is directed. Floyd-Warshall handles negative weights but not negative cycles.

Key takeaway

Floyd-Warshall's entire recurrence is one line: dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]), iterated over all intermediate vertices k.

    def floyd_warshall(self):
        """
        Computes the shortest paths between all pairs of
        nodes using the Floyd-Warshall algorithm.

        >>> g = Graph(3)
        >>> g.add_edge(0, 1, 1)
        >>> g.add_edge(1, 2, 2)
        >>> g.floyd_warshall()
        >>> g.show_min(0, 2)
        3
        >>> g.show_min(2, 0)
        inf
        """
        for k in range(self.n):
            for i in range(self.n):
                for j in range(self.n):
                    self.dp[i][j] = min(self.dp[i][j], self.dp[i][k] + self.dp[k][j])
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free