Module P-14·13 min read

Longest common subsequence, longest increasing subsequence, edit distance, and matrix path problems.

Written by Jatin Jain Saraf · Architect of SupraScan

Module P-14 — Dynamic Programming IV: LCS, LIS, Edit Distance, and Matrix Paths

What this module covers: Module P-13's 2D DP compared one sequence against a resource constraint. This module covers 2D DP where the two dimensions are two separate sequences being compared against each other — the shape behind diff tools, spell-checkers, and fuzzy matching — plus a weighted variant of Module P-13's grid-path counting.


Longest Common Subsequence (LCS)

Given two strings, find the length of the longest subsequence common to both — not necessarily contiguous, just in the same relative order.

function longestCommonSubsequence(s1: string, s2: string): number {
  const m = s1.length, n = s2.length;
  // dp[i][j] = LCS length using the first i characters of s1 and first j characters of s2
  const dp: number[][] = 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 (s1[i - 1] === s2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1; // characters match — extend the subsequence found so far
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); // no match — best of dropping a char from either side
      }
    }
  }
 
  return dp[m][n];
}
 
longestCommonSubsequence('abcde', 'ace'); // 3 — "ace"

dp[i][j] = the LCS length considering only the first i characters of s1 and the first j characters of s2. On a match, extending the LCS found in the smaller subproblem (dp[i-1][j-1], both strings one character shorter) by one is clearly correct. On a mismatch, the current characters can't both be part of the LCS together, so the best answer must come from dropping one character from one side or the other — taking whichever of those two gives the longer result. Both dimensions here represent positions within separate sequences, unlike Module P-13's knapsack, where the second dimension was a resource budget.


Longest Increasing Subsequence (LIS)

Given one sequence, find the length of its longest strictly increasing subsequence (not necessarily contiguous).

function lengthOfLIS(nums: number[]): number {
  const n = nums.length;
  if (n === 0) return 0;
 
  // dp[i] = length of the longest increasing subsequence that ENDS at index i
  const dp = new Array(n).fill(1); // every single element is trivially an increasing subsequence of length 1
 
  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1); // extend the best subsequence ending at j, if nums[i] can follow it
      }
    }
  }
 
  return Math.max(...dp);
}
 
lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]); // 4 — [2, 3, 7, 101] (or [2, 3, 7, 18])

This version is O(n²) — for each position i, it checks every earlier position j. A key subtlety worth naming: dp[i] must specifically mean "the LIS ending exactly at index i," not "the LIS within the first i elements" — because a later element might need to extend a subsequence that ends partway through the array, not necessarily the overall best-so-far. The final answer is the maximum across the whole dp array, since the overall longest subsequence could end anywhere. (An O(n log n) version exists, maintaining a separate array of "smallest tail value for each achievable subsequence length" and using Module F-11's binary search to find where each new element belongs — a genuinely different technique layered on top of the same underlying problem, not covered in full here.)


Edit Distance (Levenshtein Distance)

The minimum number of single-character insertions, deletions, or substitutions needed to transform one string into another — the exact metric behind "did you mean...?" spell-check suggestions.

function editDistance(s1: string, s2: string): number {
  const m = s1.length, n = s2.length;
  // dp[i][j] = min edits to transform the first i characters of s1 into the first j characters of s2
  const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
 
  for (let i = 0; i <= m; i++) dp[i][0] = i; // transforming to empty string: delete everything
  for (let j = 0; j <= n; j++) dp[0][j] = j; // transforming from empty string: insert everything
 
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (s1[i - 1] === s2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1]; // characters already match — no edit needed here
      } else {
        dp[i][j] = 1 + Math.min(
          dp[i - 1][j],     // delete a character from s1
          dp[i][j - 1],     // insert a character into s1
          dp[i - 1][j - 1]  // substitute a character in s1
        );
      }
    }
  }
 
  return dp[m][n];
}
 
editDistance('horse', 'ros'); // 3

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...