Algorithm

Longest Common Subsequence

Dynamic Programming Pattern

Longest Common Subsequence

You are given two strings text1 and text2. A subsequence is formed by deleting zero or more characters from a string without reordering the ones that remain. Return the length of the longest sequence of characters that appears as a subsequence in both strings. If there is no common subsequence, return 0. You only need the length, not the subsequence itself.

CONSTRAINTS
  • 1 ≤ text1.length, text2.length ≤ 1000
  • Both strings consist of lowercase English letters
  • Characters must keep their relative order but need not be adjacent
EXAMPLE 1
Input: text1 = "abcde", text2 = "ace"
Output: 3
The common subsequence "ace" appears in both — with the b and d deleted from the first string — and no length-4 sequence appears in both.
EXAMPLE 2
Input: text1 = "abc", text2 = "abc"
Output: 3
Identical strings share the whole string as a subsequence, since deleting nothing is allowed.
EXAMPLE 3
Input: text1 = "abc", text2 = "def"
Output: 0
No character appears in both strings, so the only common subsequence is the empty one, whose length is 0.
EXAMPLE 4
Input: text1 = "abcba", text2 = "abcbcba"
Output: 5
The entire first string appears inside the second with gaps, so the answer is its full length.
EXAMPLE 5
Input: text1 = "ezupkr", text2 = "ubmrapg"
Output: 2
The letters u and r appear in both strings and in the same relative order, so "ur" is common. No third letter can be added while keeping the order valid in both.
Is this asking for a subsequence or a substring?
A subsequence, so gaps are allowed as long as the order is preserved. This is the clarification that matters most, because the contiguous version is a different recurrence: on a mismatch the count resets to zero instead of taking a maximum over skips.
Should I return the length or the actual sequence?
The length. Reconstructing the sequence itself means keeping the full table and walking from the top-left cell, taking the character whenever a match was used and otherwise stepping toward whichever neighbouring cell produced the value.
Does the empty subsequence count, and what if there is no overlap?
The empty subsequence is common to every pair of strings, so the answer is 0 rather than -1 or an error when nothing matches.
Are the strings the same length, and how long can they get?
They can differ in length, and each can reach 1000 characters, which makes an N × M table of a million cells — comfortable. If they could reach a million characters each, this quadratic approach would be off the table entirely.
Is the comparison case-sensitive?
The inputs are all lowercase here, so it does not arise. With mixed case you would confirm whether 'A' should match 'a' before writing anything, since it changes the equality test at the heart of the recurrence.
Subsequence, not substring

First get the definition exactly right, because everything downstream depends on it. A substring is a contiguous run of characters: in "abcde", "bcd" is a substring but "ace" is not. A subsequence allows gaps, as long as the surviving characters keep their original left-to-right order: "ace" is a subsequence of "abcde", and so is "abcde" itself, and so is the empty string. "aec" is not, because e comes after c in the original.

So we want the longest string that can be carved out of both inputs by deleting characters. For "abcde" and "ace", the answer is "ace" with length 3.

Why you cannot just scan for matches

A tempting idea: walk both strings with a pointer each, and every time the characters match, count it and advance both; otherwise advance one of them. But which one? That single question is the whole problem, because the choice you make now determines what remains matchable later.

Try text1 = "abcde", text2 = "aed". After matching a, we compare b against e. Advance the first string and we eventually match e and then d? No — after e in text2 comes d, and in text1 after e there is nothing. So that route gives "ae", length 2. Advance the other pointer instead and we compare b against d, eventually matching d, giving "ad", also 2. Here they tie, but it is easy to build cases where one choice is strictly better, and there is no local test that reveals which. When the right move depends on the whole rest of the input, you must try both moves and compare.

The brute force version of "try everything" is genuinely awful: enumerate all 2ⁿ subsequences of the first string and test each against the second. For 1000-character strings that is beyond hopeless.

The state: two prefixes

State Definition: f(i, j) is the length of the longest common subsequence of the first i characters of text1 and the first j characters of text2.

The recurrence

Look at the last character on each side, text1[i-1] and text2[j-1]:

- They match. Pair them, add 1, and drop both — solve the rest on i-1 and j-1: 1 + f(i-1, j-1). Taking the match is always safe: pairing these two never blocks a better answer later, so there is no reason to refuse it.
- They differ. At least one of the two cannot be in the answer. We do not know which, so drop one side, try the other, and keep the better: max( f(i-1, j), f(i, j-1) ).

Base case: if either count reaches 0, that string is empty and nothing is common — f(0, j) = f(i, 0) = 0.

We start with the full lengths: f(n, m).

Recursion and memo
python
def lcs(text1, text2):
    n, m = len(text1), len(text2)
    memo = {}

    def f(i, j):
        if i == 0 or j == 0: return 0
        if (i, j) in memo: return memo[(i, j)]
        if text1[i - 1] == text2[j - 1]:
            result = 1 + f(i - 1, j - 1)
        else:
            result = max(f(i - 1, j), f(i, j - 1))
        memo[(i, j)] = result
        return result

    return f(n, m)

There are n × m states, each doing constant work: O(N × M) time — a million steps for 1000-character strings, which is nothing.

The table from the recurrence

Let dp[i][j] hold what f(i, j) returned, and swap each call for an array read: on a match dp[i][j] = 1 + dp[i-1][j-1], otherwise dp[i][j] = max( dp[i-1][j], dp[i][j-1] ).

Every read points at a smaller i or j, so both loops run upward from 1, and every cell is ready when needed. Row 0 and column 0 are all zeros — the base case written as data. The answer is dp[n][m]:

python
def lcs(text1, text2):
    n, m = len(text1), len(text2)
    dp = [[0] * (m + 1) for _ in range(n + 1)]   # row 0 and column 0 are the zero base case

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if text1[i - 1] == text2[j - 1]:
                dp[i][j] = 1 + dp[i - 1][j - 1]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    return dp[n][m]

Those zero row and column are not padding for convenience — they are the base case, written as data instead of an if. That trick saves a bounds check in almost every grid-shaped table you will write.

Two rows are enough

Row i reads only row i and row i-1, never anything further. So keep two:

python
def lcs(text1, text2):
    n, m = len(text1), len(text2)
    prev = [0] * (m + 1)                  # row i-1
    for i in range(1, n + 1):
        cur = [0] * (m + 1)
        for j in range(1, m + 1):
            if text1[i - 1] == text2[j - 1]:
                cur[j] = 1 + prev[j - 1]
            else:
                cur[j] = max(prev[j], cur[j - 1])
        prev = cur
    return prev[m]

O(M) space. Make the shorter string the inner dimension and it is O(min(N, M)).

Worked Example:text1 = "abcde", text2 = "ace"
Fill from the top-left. The matches land on a, c, e, and each one adds 1 to the diagonal cell behind it:
- dp[1][1]: a vs a — match. 1 + dp[0][0] = 1
- dp[3][2]: c vs c — match. 1 + dp[2][1] = 1 + 1 = 2
- dp[5][3]: e vs e — match. 1 + dp[4][2] = 1 + 2 = 3

Between the matches, a cell just carries the larger of its neighbour above or to the left. The answer is dp[5][3] = 3, the subsequence "ace".

And a case with no overlap at all, "abc" versus "def": every comparison differs, every cell copies a zero neighbour, and the answer is 0 — the empty subsequence, which is always common to any two strings.

The shape to recognise

This is the parent of a large family. The signal is: two sequences, and progress means moving forward in one or both of them. That gives a two-dimensional state (i, j) and a small menu of moves — match and take both, or drop a character from one side.

The variations only change which moves are allowed and what they cost. Some minimise a cost instead of maximising a length. Some add a third move. Some forbid the skips, so a mismatch resets to 0. The grid stays the same; only the moves change.

When you see two sequences and a question about lining them up, reach for the (i, j) grid first, then work out the moves.

Exponential Try Every Subsequence
O(N × M) Time · O(N × M) Space Memoisation
O(N × M) Time · O(N × M) Space Tabulation
O(N × M) Time · O(min(N, M)) Space Two Rows