Skip to content

Coding patternsBlind 75

Longest Common Subsequence

Fill a table where each cell is the answer for two prefixes, built from the cells above, to the left and on the diagonal; then keep only two rows. O(m × n) time and O(n) space, with a step-through, code in five languages and the Big O worked out from the code.

Given two strings text1 and text2, return the length of their longest common subsequence: the longest string you can get from both by deleting some letters, without changing the order of the rest. If they share no letters, return 0.

Example 1

Input: text1 = "ocean", text2 = "clean"

Output: 4

Drop the “o” from “ocean” and the “l” from “clean”, and both become “cean”.

text1
ocean
text2
clean

Example 2

Input: text1 = "abc", text2 = "xyz"

Output: 0

No letter appears in both.

Example 3

Input: text1 = "stone", text2 = "notes"

Output: 2

For example “oe” or “te”. The words share all five letters, but in different orders.

  • 1 ≤ text1.length, text2.length ≤ 1000 (call them m and n)
  • Only lowercase English letters.
Must the common letters be next to each other?
No. A subsequence may skip letters, as long as the order is kept.
Do we return the subsequence or its length?
Only the length.
Can a letter be matched twice?
No. Each letter of each string is used at most once.

Compare the last letters of the two strings. If they are equal, they can be matched with each other, and the rest is the same question on both strings minus one letter. If not, at least one of them is unused, so drop it and ask again. Answers for pairs of prefixes, built from smaller pairs that repeat, are dynamic programming on a two-dimensional table.

There is no brute force tab. Trying every subsequence of one string against the other takes 2ᵐ tries, and the plain recursion on prefixes is also exponential. The table is the standard first answer.

Two rows O(m × n)

A cell reads only three others: the one above, the one on the diagonal above-left, and the one to its left. The first two are in the previous row, and the last is in the current row. So keep just two rows: previous_row and current_row.

Fill current_row left to right with the same rule, reading previous_row[j - 1], previous_row[j] and current_row[j - 1]. When the row is done, current_row becomes previous_row and a fresh current_row starts.

Why it is correct: it computes the same numbers as the full table, in the same order, and only drops rows that are never read again.

text2
""clean
previous_row
000000
  1. previous_row = [0] * (len(text2) + 1)
  2. for i in 1 .. len(text1):
  3. current_row = [0] * (len(text2) + 1)
  4. for j in 1 .. len(text2):
  5. if letters match: current_row[j] = previous_row[j - 1] + 1
  6. else: current_row[j] = max(previous_row[j], current_row[j - 1])
  7. previous_row = current_row
  8. return previous_row[len(text2)]

A table cell only reads the row above it (above and diagonal) and the cell to its left. So keep two rows: previous_row, the row above, and current_row, the row being filled.

1 / 11
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Two rows: each row of the table only reads the row above it, so keep the
// previous row and the current one. O(m × n) time, O(n) space.
public int longestCommonSubsequence(String text1, String text2) {
int text2Length = text2.length();
int[] previousRow = new int[text2Length + 1];
for (int i = 1; i <= text1.length(); i++) {
// cur[0] stays 0: an empty prefix of text2 shares nothing.
int[] currentRow = new int[text2Length + 1];
for (int j = 1; j <= text2Length; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
// Equal last letters: use them, plus the best for both prefixes without them.
currentRow[j] = previousRow[j - 1] + 1;
} else {
// Different: one of the two last letters is not used.
currentRow[j] = Math.max(previousRow[j], currentRow[j - 1]);
}
}
previousRow = currentRow;
}
return previousRow[text2Length];
}
}
Time O(m × n)Space O(n)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    Same as the full table: each cell filled is 1 step.

    1. previous_row = [0] * (len(text2) + 1)
    2. for i in 1 .. len(text1):
    3. current_row = [0] * (len(text2) + 1)
    4. for j in 1 .. len(text2):
    5. if letters match: current_row[j] = previous_row[j - 1] + 1
    6. else: current_row[j] = max(previous_row[j], current_row[j - 1])
    7. previous_row = current_row
    8. return previous_row[len(text2)]
  2. 2 Count the runs on Example 1

    The same 5 rows of 5 cells.

    25 cells.

    row1 "o"2 "c"3 "e"4 "a"5 "n"
    cells filled55555
    total
    5 × 5 = 25 cells
  3. 3 Write the count for any n

    m × n cells

    Check it with Example 1: 5 × 5 = 25. That matches the count.

  4. 4 Keep only the part that grows fastest

    Nothing to drop. The time is still O(m × n). Only the memory improves.

    So the time is O(m × n).

  5. S Space: count the extra memory the same way

    Two rows of n + 1 numbers. So the space is O(n). Passing the shorter string as text2 makes it O(min(m, n)).

    previous_row (n + 1)current_row (n + 1)

2,000 random pairs of texts of 1 to 8 letters, using only a and b, or a, b and c, so letters repeat often. The full table and the two rows gave the same answer every time, and matched a check that tries every subsequence.

Largest inputsResult
Two texts of 1,000 letters (the most allowed), random letters a to d. Two rows.0.09 s, and about 0.04 MB of extra memory: two rows of 1,001 counts.
The same two texts. Full table.0.1 s, and about 11 MB of extra memory: a table of 1,001 x 1,001 counts. The two rows give the same answer with far less memory.