Longest Common Subsequence and Optimal Binary Search Trees Explained

Two more classic dynamic programming problems reveal the technique's versatility beyond numeric optimization: finding the longest common subsequence between two strings, a cornerstone of diff tools and bioinformatics, and constructing a binary search tree that minimizes expected search cost given known access frequencies. This comprehensive guide walks through both algorithms in full detail, including recurrence derivation, table construction, and solution reconstruction.

Longest Common SubsequenceOptimal Binary Search TreeLCS Algorithm

~6 دقیقه مطالعه · آخرین به‌روزرسانی ۱۶ شهریور ۱۴۰۵

The Longest Common Subsequence Problem

A Subsequence of a string is derived by deleting zero or more characters without changing the order of the remaining characters. The Longest Common Subsequence (LCS) problem asks: given two sequences, find the longest subsequence common to both.

Example:
X = "ABCBDAB"
Y = "BDCABA"

One common subsequence: "BCBA" (length 4)
Another: "BDAB" (length 4)
The LCS in this case has length 4

This problem underlies the diff utility used to compare file versions, DNA sequence comparison in bioinformatics, and plagiarism detection tools, making it one of the most practically significant applications of dynamic programming.

Establishing Optimal Substructure

Let Xᵢ denote the first i characters of string X, and similarly for Yⱼ. The key structural insight considers the last characters of both sequences.

If X[i] == Y[j]:
    the LCS of Xᵢ and Yⱼ extends the LCS of Xᵢ₋₁ and Yⱼ₋₁
    by exactly this matching character

If X[i] ≠ Y[j]:
    the LCS of Xᵢ and Yⱼ is the longer of:
    - the LCS of Xᵢ₋₁ and Yⱼ (drop the last character of X)
    - the LCS of Xᵢ and Yⱼ₋₁ (drop the last character of Y)

This gives the recurrence for c[i][j], the length of the LCS of Xᵢ and Yⱼ:

c[i][j] = 0                              if i == 0 or j == 0
c[i][j] = c[i-1][j-1] + 1                if i,j > 0 and X[i] == Y[j]
c[i][j] = max(c[i-1][j], c[i][j-1])      if i,j > 0 and X[i] ≠ Y[j]

Building the Solution Bottom-Up

LCS-LENGTH(X, Y, m, n):
  let c[0..m][0..n] and b[1..m][1..n] be new tables
  for i = 1 to m:
      c[i][0] = 0
  for j = 0 to n:
      c[0][j] = 0
  for i = 1 to m:
      for j = 1 to n:
          if X[i] == Y[j]:
              c[i][j] = c[i-1][j-1] + 1
              b[i][j] = "diagonal"
          elif c[i-1][j] ≥ c[i][j-1]:
              c[i][j] = c[i-1][j]
              b[i][j] = "up"
          else:
              c[i][j] = c[i][j-1]
              b[i][j] = "left"
  return c, b

Tracing through the earlier example with X = "ABCBDAB" and Y = "BDCABA" fills a table where c[7][6] ultimately holds the value 4, matching the LCS length found by inspection above.

Since the table has Θ(mn) entries and each takes O(1) time to fill given the entries it depends on, the algorithm runs in Θ(mn) time — a dramatic improvement over the exponential number of possible subsequences that a naive brute-force approach would need to examine.

Reconstructing the Actual Subsequence

The auxiliary table b records which case applied at each cell, allowing the actual longest common subsequence, not just its length, to be reconstructed by tracing backward from b[m][n] to the origin, following "diagonal" moves to collect matching characters, and "up" or "left" moves to skip non-matching positions.

Optimal Binary Search Trees: A Different Kind of Optimization

The binary search tree discussed earlier in this series assumes every key is equally likely to be searched. In many real applications, some keys are searched far more frequently than others, and the tree's shape should reflect this: frequently accessed keys should sit closer to the root, minimizing their search cost, even if this means less-frequently accessed keys sit deeper.

Given n distinct keys with known search probabilities p₁, ..., pₙ, the Optimal Binary Search Tree problem asks for the binary search tree structure that minimizes the expected total search cost.

Expected search cost of a tree T:
E[search cost] = Σ (i=1 to n) (depth_T(kᵢ) + 1) · pᵢ

where depth_T(kᵢ) is the depth of key kᵢ in tree T
(root has depth 0, so accessing it costs 1 comparison)

Establishing the Recursive Structure

The key insight, similar in spirit to matrix-chain multiplication discussed earlier in this series, is that an optimal binary search tree over a contiguous range of keys has a recursive structure: whichever key is chosen as the root of a subtree, the keys smaller than it must form the left subtree, and the keys larger than it must form the right subtree, and each of these subtrees must itself be optimal for its respective range and set of probabilities.

Let e[i][j] = expected cost of an optimal BST
              containing keys kᵢ through kⱼ

Let w[i][j] = sum of probabilities pᵢ through pⱼ
              (this accounts for the cost increase of
               adding one more level to every node
               in the subtree, since choosing a root
               increases every descendant's depth by 1)

e[i][j] = min over r (i ≤ r ≤ j) of:
            e[i][r-1] + e[r+1][j] + w[i][j]

Base case: e[i][i-1] = 0  (empty subtree)

The term w[i][j] is added regardless of which root r is chosen, since selecting any node as a subtree's root increases the depth of every other node in that subtree by exactly one level, adding w[i][j] to the total expected cost no matter how the subtree is further structured internally.

Filling the Table Bottom-Up

OPTIMAL-BST(p, n):
  let e[1..n+1][0..n], w[1..n+1][0..n], root[1..n][1..n] be new tables
  for i = 1 to n + 1:
      e[i][i-1] = 0
      w[i][i-1] = 0
  for length = 1 to n:
      for i = 1 to n - length + 1:
          j = i + length - 1
          e[i][j] = infinity
          w[i][j] = w[i][j-1] + p[j]
          for r = i to j:
              t = e[i][r-1] + e[r+1][j] + w[i][j]
              if t < e[i][j]:
                  e[i][j] = t
                  root[i][j] = r
  return e, root

The algorithm proceeds by increasing subtree length, exactly as in matrix-chain multiplication, since e[i][j] depends only on entries involving strictly shorter ranges. With Θ(n²) table entries, each requiring O(n) work to try every possible root, the total running time is Θ(n³), matching matrix-chain multiplication's complexity exactly, a reflection of the structural similarity between the two problems.

Why These Two Problems Illustrate Dynamic Programming's Range

Longest common subsequence and optimal binary search trees, despite solving very different practical problems — string comparison versus data structure optimization — both follow the same fundamental dynamic programming pattern established earlier in this series: identify optimal substructure by considering the last decision made (which characters match, or which key becomes the root), express the solution as a recurrence over subproblems, and fill a table bottom-up in an order that ensures every dependency is already computed. Recognizing this shared pattern across seemingly unrelated problems is the key skill for applying dynamic programming to new problems not yet seen.

نوشته و پژوهش‌شده توسط دکتر شاهین صیامی

مقالات مرتبط

Number-Theoretic Algorithms: GCD, Modular Exponentiation, and RSA

Modern cryptography and countless algorithmic applications rely on a handful of elegant number-theoretic algorithms. This comprehensive guide covers Euclid's algorithm for computing the greatest common divisor, fast modular exponentiation for efficiently computing large powers, and the mathematical foundation of RSA encryption, one of the most widely deployed cryptographic systems in the world.

ادامه

Computational Geometry Basics: Orientation, Line Intersection, and Convex Hull

Geometric algorithms solve problems involving points, lines, and shapes, appearing in computer graphics, robotics path planning, and geographic information systems. This comprehensive guide covers the cross-product-based orientation test that underlies nearly every geometric algorithm, segment intersection detection built on that test, and Graham's scan algorithm for computing the convex hull of a set of points.

ادامه

String Matching Algorithms: Naive Search, Rabin-Karp, and Beyond

Searching for a pattern within a larger text is one of the most common operations in computing, from text editors to DNA sequence analysis. This comprehensive guide covers the naive string-matching algorithm and its quadratic worst case, then explains the Rabin-Karp algorithm's clever use of hashing to achieve fast average-case performance, including how it handles hash collisions correctly.

ادامه

Approximation Algorithms: Getting Provably Close to Optimal for Hard Problems

When a problem is proven NP-complete, an exact efficient solution is unlikely to exist, but that does not mean giving up on the problem entirely. This comprehensive guide explains approximation algorithms, which sacrifice guaranteed optimality for guaranteed efficiency, covering the vertex cover and traveling salesman problems as classic examples with provable approximation ratios.

ادامه

NP-Completeness Explained: P, NP, and Why Some Problems Resist Efficient Solutions

Some problems have resisted every attempt at an efficient algorithm for decades, yet no one has proven an efficient solution is impossible. This comprehensive guide explains the classes P and NP, the concept of polynomial-time reductions used to compare problem difficulty, and how proving a problem NP-complete provides strong evidence, though not proof, that no efficient algorithm exists.

ادامه

Maximum Flow: Ford-Fulkerson and the Min-Cut Max-Flow Theorem

Maximum flow problems model the largest possible throughput through a network with capacity-limited connections, from water pipes to data networks. This comprehensive guide introduces flow networks, walks through the Ford-Fulkerson method for finding maximum flow using augmenting paths, and explains the elegant min-cut max-flow theorem that connects two seemingly different problems into one.

ادامه