Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Dynamic programming

Longest common subsequence

Lesson · 12 min read · Dynamic programming · Longest common subsequence

The longest sequence that appears, in order, in two strings. A table over pairs of prefixes, filled row by row.

The problem

A common subsequence of two strings appears in both, in order, not necessarily contiguously. For ABCBDAB and BDCABA, BCBA is common and no common subsequence is longer, so the answer is 4. This is what diff computes to show which lines two files share.

The state: two prefixes

Compare the last characters of the two prefixes a[:i] and b[:j]:

  • If they are equal, an optimal common subsequence can use that character as its last one: 1 + L(i − 1, j − 1).
  • Otherwise at least one of them is not in it: drop one or the other, max(L(i − 1, j), L(i, j − 1)).

Base cases: an empty prefix has nothing in common with anything, L(0, j) = L(i, 0) = 0. The state (i, j) ranges over (n + 1) × (m + 1) pairs of prefix lengths, which is why the table has an extra row and column.

from functools import lru_cache

def lcs(a, b):
    @lru_cache(maxsize=None)
    def L(i, j):                      # LCS of a[:i] and b[:j]
        if i == 0 or j == 0:
            return 0
        if a[i - 1] == b[j - 1]:
            return 1 + L(i - 1, j - 1)
        return max(L(i - 1, j), L(i, j - 1))
    return L(len(a), len(b))

Bottom-up

Each cell reads the cells above, to the left and diagonally up-left, so row by row works.

The table for ABCBDAB (rows) and BDCABA (columns), one row per frame.

  1. Step 1 / 8
    ∅BDCABA∅0000000A0B0C0B0D0A0B0

    Base cases filled first.

  2. Step 2 / 8
    ∅BDCABA∅0000000A0000111B0C0B0D0A0B0

    Row A filled.

  3. Step 3 / 8
    ∅BDCABA∅0000000A0000111B0111122C0B0D0A0B0

    Row B filled.

  4. Step 4 / 8
    ∅BDCABA∅0000000A0000111B0111122C0112222B0D0A0B0

    Row C filled.

  5. Step 5 / 8
    ∅BDCABA∅0000000A0000111B0111122C0112222B0112233D0A0B0

    Row B filled.

  6. Step 6 / 8
    ∅BDCABA∅0000000A0000111B0111122C0112222B0112233D0122233A0B0

    Row D filled.

  7. Step 7 / 8
    ∅BDCABA∅0000000A0000111B0111122C0112222B0112233D0122233A0122334B0

    Row A filled.

  8. Step 8 / 8
    ∅BDCABA∅0000000A0000111B0111122C0112222B0112233D0122233A0122334B0122344

    Row B filled. The bottom-right cell, 4, is the answer. Walking back from it (diagonal on a match, otherwise towards the larger neighbour) spells out one longest common subsequence.

Implementation

In six languages:

def lcs_length(text1, text2):
    m = len(text2)
    prev = [0] * (m + 1)                       # row i - 1 of the table
    for ch in text1:
        cur = [0] * (m + 1)
        for j in range(1, m + 1):
            if ch == text2[j - 1]:
                cur[j] = prev[j - 1] + 1       # match: extend the diagonal
            else:
                cur[j] = max(prev[j], cur[j - 1])
        prev = cur
    return prev[m]

The same code is the reference solution to Longest common subsequence, where it is compiled and run against the tests in all six languages.

TimeO(n · m)
SpaceO(n · m), or O(min(n, m)) keeping two rows