Skip to content
DSA dynamic programming 8 min read

Longest Common Subsequence (LCS): A Classic 2D Dynamic Programming

The Longest Common Subsequence (LCS) of two strings is the longest sequence of characters that appears in both, in the same relative order but not necessarily contiguous. For "ABCBDAB" and "BDCAB", one LCS is "BCAB" (length 4). LCS is the foundational two-sequence DP — it underpins diff tools, version control, and DNA alignment. This page derives the 2D table and shows how to recover the actual subsequence.

Subsequence vs substring

A substring is contiguous ("BCB" from "ABCBDAB"). A subsequence only keeps order — you may delete characters but not reorder them ("ABD" is a subsequence of "ABCBDAB"). LCS deals with subsequences.

Defining the DP

Compare the two strings from their fronts (or backs). Look at the last characters of the first i of a and the first j of b:

  • State: dp[i][j] = length of the LCS of a[0..i-1] and b[0..j-1].
  • Transition:
    • if a[i-1] == b[j-1]: they extend a common subsequence → dp[i][j] = 1 + dp[i-1][j-1].
    • else: drop one character → dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
  • Base cases: dp[0][j] = dp[i][0] = 0 — an empty string shares nothing.

The answer is dp[m][n].

Tabulation: O(mn) time, O(mn) space

int lcs(const std::string& a, const std::string& b) {
    int m = a.size(), n = b.size();
    std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            if (a[i - 1] == b[j - 1])
                dp[i][j] = 1 + dp[i - 1][j - 1];
            else
                dp[i][j] = std::max(dp[i - 1][j], dp[i][j - 1]);
    return dp[m][n];
}

Reconstructing the subsequence

The table holds the length; to recover the actual characters, walk backward from dp[m][n]. When characters match, that character is part of the LCS; otherwise step toward the larger neighbor.

std::string lcsString(const std::string& a, const std::string& b) {
    int m = a.size(), n = b.size();
    std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            dp[i][j] = (a[i - 1] == b[j - 1]) ? dp[i - 1][j - 1] + 1
                                              : std::max(dp[i - 1][j], dp[i][j - 1]);
    std::string res;
    int i = m, j = n;
    while (i > 0 && j > 0) {
        if (a[i - 1] == b[j - 1]) { res += a[i - 1]; i--; j--; }
        else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
        else j--;
    }
    std::reverse(res.begin(), res.end());
    return res;
}

For beginners: If you only need the length, two rolling rows reduce space to O(n). But reconstructing the string requires the full table (or a clever divide-and-conquer), so keep the 2D grid when you need the characters back.

Complexity

GoalTimeSpace
LCS lengthO(mn)O(mn), or O(n) rolling
LCS stringO(mn)O(mn)

Pitfall: Don’t confuse LCS with longest common substring (contiguous) — the substring version resets dp[i][j] to 0 on a mismatch and tracks the global max, not dp[m][n].

LCS is the springboard to edit distance and many subsequence DPs. Next up: Longest Increasing Subsequence.

Last updated June 25, 2026
Was this helpful?