Longest Common Subsequence

Difficulty: Medium

You're given two strings. Find the length of their longest common subsequence — the longest sequence of characters that appears in both strings in the same relative order, but not necessarily touching each other (you're allowed to skip characters in either string).

For example, "ace" is a subsequence of "abcde" because you can delete "b" and "d" and still be left with "ace" in order.

Examples

Input: text1 = "abcde", text2 = "ace"
Output: 3

"ace" is the longest string that is a subsequence of both.

Input: text1 = "abc", text2 = "abc"
Output: 3

Input: text1 = "abc", text2 = "def"
Output: 0

The two strings share no characters at all.

Constraints

  • 1 <= text1.length, text2.length <= 1000

  • Both strings contain only lowercase English letters

Approach

Think of building a table where cell (i, j) answers: "what is the longest common subsequence between the first i characters of text1 and the first j characters of text2?"

If the i-th character of text1 and the j-th character of text2 are the same letter, that letter can always be safely used in the answer — so the answer for (i, j) is 1 plus the answer for (i-1, j-1) (both strings with that matching character removed).

If they're different letters, the matching character isn't shared at this position, so the best you can do is whichever is better: dropping the last character of text1 (answer for (i-1, j)) or dropping the last character of text2 (answer for (i, j-1)).

An empty prefix of either string can never share anything, so the first row and first column of the table are all 0 — that's your base case.

Solutions

Brute Force — Recursion

Walk both strings from the front. If the current characters match, take that character and recurse on both strings advanced by one. Otherwise, try skipping a character from either string and keep the better result. This revisits the same (i, j) pairs repeatedly.

function longestCommonSubsequence(text1, text2) {
  function lcs(i, j) {
    if (i === text1.length || j === text2.length) return 0;
    if (text1[i] === text2[j]) return 1 + lcs(i + 1, j + 1);
    return Math.max(lcs(i + 1, j), lcs(i, j + 1));
  }
  return lcs(0, 0);
}

Time: O(2^(m + n)) · Space: O(m + n) — recursion depth

Optimal — Bottom-Up 2D Table

Build a table indexed by (prefix length of text1, prefix length of text2). Row and column 0 start at 0 because an empty prefix shares nothing. Fill the rest left to right, top to bottom: extend the diagonal answer by one on a match, otherwise carry forward the better of the cell above or the cell to the left.

function longestCommonSubsequence(text1, text2) {
  const m = text1.length;
  const n = text2.length;
  const table = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (text1[i - 1] === text2[j - 1]) {
        table[i][j] = table[i - 1][j - 1] + 1;
      } else {
        table[i][j] = Math.max(table[i - 1][j], table[i][j - 1]);
      }
    }
  }

  return table[m][n];
}

Time: O(m * n) · Space: O(m * n)