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 substring

Lesson · 9 min read · Dynamic programming · Longest common substring

Like the common subsequence, but the shared part must be contiguous. One change to the recurrence: a mismatch resets to zero.

Contiguous changes the recurrence

A substring (for lists, a subarray) has no gaps. For [1, 2, 3, 2, 1] and [3, 2, 1, 4, 7] the longest shared block is 3, 2, 1, length 3.

The prefix state of the subsequence problem does not work directly, because a block that has been broken cannot be continued. Use the same trick as for the increasing subsequence: fix the end. Let E(i, j) be the length of the longest common block ending exactly at a[i − 1] and b[j − 1].

Recurrence

E(i, j)

  1. if a[i − 1] = b[j − 1]: 1 + E(i − 1, j − 1) (extend the block ending just before);
  2. otherwise 0 (no common block ends here).

answer

  1. the largest E(i, j) anywhere in the table.

Bottom-up

E(i, j) for [1, 2, 3, 2, 1] and [3, 2, 1, 4, 7]. Matches extend along diagonals; mismatches are 0.

  1. Step 1 / 6
    ∅32147∅0000001020302010

    Base cases filled first.

  2. Step 2 / 6
    ∅32147∅000000100010020302010

    Row 1 filled.

  3. Step 3 / 6
    ∅32147∅00000010001002001000302010

    Row 2 filled.

  4. Step 4 / 6
    ∅32147∅0000001000100200100030100002010

    Row 3 filled.

  5. Step 5 / 6
    ∅32147∅000000100010020010003010000200200010

    Row 2 filled.

  6. Step 6 / 6
    ∅32147∅00000010001002001000301000020020001000300

    Row 1 filled. The largest entry is 3: the block 3, 2, 1, read along its diagonal.

Implementation

Each row reads only the previous row, so two rows are enough:

def longest_common_block(nums1, nums2):
    m = len(nums2)
    prev = [0] * (m + 1)                # E for the previous row
    best = 0
    for x in nums1:
        cur = [0] * (m + 1)
        for j in range(1, m + 1):
            if x == nums2[j - 1]:
                cur[j] = prev[j - 1] + 1     # extend the block ending one step earlier
                best = max(best, cur[j])
        prev = cur
    return best

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

TimeO(n · m)
SpaceO(m)

The answer is not in the corner of the table, as for the subsequence: it is the maximum anywhere. That difference, "ends exactly here" against "within the first i and j", recurs throughout DP.