Subsequence Problems
Subsequence DP compares two strings (or one string against itself) by building a 2D table over prefixes. This lesson covers the longest common subsequence (LCS, LeetCode 1143), longest palindromic subsequence (516), related variants, and full solutions in C, Java, and Python with trace tables.
Overview
A subsequence keeps characters in order but may skip any number of them. Subsequence DP typically uses:
State: dp[i][j] = answer for first i chars of A and first j chars of B
Base: dp[0][*] = dp[*][0] = 0 (empty prefix)
Match: dp[i][j] = dp[i-1][j-1] + 1 (or +2 for palindrome pairs)
Mismatch: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) (LCS)
dp[i][j] = max(dp[i+1][j], dp[i][j-1]) (LPS, fill backwards)
| Problem | Goal | State | LeetCode |
|---|---|---|---|
| LCS | Longest common subsequence of two strings | dp[i][j] on prefixes | 1143 |
| LPS | Longest palindromic subsequence in one string | dp[i][j] on substring s[i..j] | 516 |
| Delete ops for two strings | Min deletions to make equal | LCS length → m+n−2·LCS | 583 |
| Distinct subsequences | Count s2 subsequences in s1 | dp[i][j] count | 115 |
| Longest common substring | Contiguous match only | Reset to 0 on mismatch | Custom |
Subsequence vs Substring vs Subarray
| Term | Contiguous? | Order preserved? | Example in "abcde" |
|---|---|---|---|
| Subsequence | No | Yes | "ace", "bd", "a" |
| Substring | Yes | Yes | "bcd", "ab", "e" |
| Subarray | Yes (array) | Yes | Same as substring for strings |
dp[i][j]=0, not max of neighbors.
Longest Common Subsequence (LCS)
Problem (LeetCode 1143): Given text1 and text2, return the length of their longest common subsequence.
State: dp[i][j] = LCS length of text1[0..i−1] and text2[0..j−1].
Recurrence:
- If
text1[i−1] == text2[j−1]:dp[i][j] = dp[i−1][j−1] + 1 - Else:
dp[i][j] = max(dp[i−1][j], dp[i][j−1])
Answer: dp[m][n] where m, n are string lengths.
LCS Code (C, Java, Python)
Python
def longest_common_subsequence(text1, text2):
"""LeetCode 1143 — LCS length of two strings. O(m × n)."""
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # match: extend LCS
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # skip one char
return dp[m][n]
if __name__ == "__main__":
tests = [("abcde", "ace"), ("abc", "abc"), ("abc", "def")]
for t1, t2 in tests:
print(f"LCS('{t1}', '{t2}') = {longest_common_subsequence(t1, t2)}")
Java
public class LongestCommonSubsequence {
/** LCS length — classic 2D DP on string prefixes. */
public static int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
System.out.println(longestCommonSubsequence("abcde", "ace")); // 3
System.out.println(longestCommonSubsequence("abc", "abc")); // 3
System.out.println(longestCommonSubsequence("abc", "def")); // 0
}
}
C
#include <stdio.h>
#include <string.h>
#define MAX 1001
int max2(int a, int b) { return a > b ? a : b; }
/* LCS length of two null-terminated strings */
int lcs(char *text1, char *text2) {
int dp[MAX][MAX];
int m = (int)strlen(text1), n = (int)strlen(text2);
int i, j;
for (i = 0; i <= m; i++) dp[i][0] = 0;
for (j = 0; j <= n; j++) dp[0][j] = 0;
for (i = 1; i <= m; i++) {
for (j = 1; j <= n; j++) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max2(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
int main(void) {
printf("%d\n", lcs("abcde", "ace")); /* 3 */
printf("%d\n", lcs("abc", "abc")); /* 3 */
printf("%d\n", lcs("abc", "def")); /* 0 */
return 0;
}
LCS Output & Trace
Program output (all three languages)
| Language | Input | Output | One LCS |
|---|---|---|---|
| Python | "abcde", "ace" | 3 | "ace" |
| Python | "abc", "abc" | 3 | "abc" (full string) |
| Python | "abc", "def" | 0 | No common char |
| Java / C | same tests | 3, 3, 0 | Identical logic |
DP table trace — text1 = "abcde", text2 = "ace"
| dp[i][j] | "" | a | c | e |
|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 |
| a | 0 | 1 | 1 | 1 |
| b | 0 | 1 | 1 | 1 |
| c | 0 | 1 | 2 | 2 |
| d | 0 | 1 | 2 | 2 |
| e | 0 | 1 | 2 | 3 |
Longest Palindromic Subsequence
Problem (LeetCode 516): Find the length of the longest subsequence of s that reads the same forward and backward.
State: dp[i][j] = LPS length in substring s[i..j].
Recurrence:
- Base:
dp[i][i] = 1(single character) - If
s[i] == s[j]:dp[i][j] = dp[i+1][j−1] + 2 - Else:
dp[i][j] = max(dp[i+1][j], dp[i][j−1])
Fill i from bottom to top so inner subproblems are ready.
LPS Code (C, Java, Python)
Python
def longest_palindrome_subseq(s):
"""LeetCode 516 — longest palindromic subsequence length."""
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
dp[i][i] = 1
for j in range(i + 1, n):
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2 # pair outer chars
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][n - 1]
if __name__ == "__main__":
for s in ["bbbab", "cbbd", "a"]:
print(f"LPS('{s}') = {longest_palindrome_subseq(s)}")
Java
public class LongestPalindromeSubseq {
public static int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = n - 1; i >= 0; i--) {
dp[i][i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
public static void main(String[] args) {
System.out.println(longestPalindromeSubseq("bbbab")); // 4
System.out.println(longestPalindromeSubseq("cbbd")); // 2
System.out.println(longestPalindromeSubseq("a")); // 1
}
}
C
#include <stdio.h>
#include <string.h>
#define MAX 1001
int max2(int a, int b) { return a > b ? a : b; }
int lps(char *s) {
int dp[MAX][MAX];
int n = (int)strlen(s);
int i, j;
for (i = n - 1; i >= 0; i--) {
dp[i][i] = 1;
for (j = i + 1; j < n; j++) {
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = max2(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][n - 1];
}
int main(void) {
printf("%d\n", lps("bbbab")); /* 4 */
printf("%d\n", lps("cbbd")); /* 2 */
printf("%d\n", lps("a")); /* 1 */
return 0;
}
LPS Output & Trace
Program output
| Language | Input | Output | One palindromic subseq |
|---|---|---|---|
| Python | "bbbab" | 4 | "bbbb" |
| Python | "cbbd" | 2 | "bb" |
| Python | "a" | 1 | "a" |
| Java / C | same tests | 4, 2, 1 | Same results |
Key cells — s = "bbbab" (indices 0..4)
| Subproblem s[i..j] | dp[i][j] | Why |
|---|---|---|
| [4..4] "b" | 1 | Single char base |
| [3..4] "ab" | 1 | a ≠ b → max of [4..4], [3..3] |
| [1..3] "bba" | 2 | Pair b's at ends → "bb" |
| [0..4] "bbbab" | 4 | Best: "bbbb" via outer b's + inner "bb" |
Related Subsequence Problems
| Problem | Key insight | Formula / twist | LeetCode |
|---|---|---|---|
| Delete operation for two strings | Keep LCS, delete the rest | Answer = len(s1)+len(s2) − 2·LCS | 583 |
| Shortest common supersequence | Cover both strings | len(s1)+len(s2) − LCS | 1092 |
| Distinct subsequences | Count matchings | On match: dp[i−1][j−1] + dp[i−1][j] | 115 |
| Longest common substring | Contiguous only | On mismatch: dp[i][j]=0 | Custom |
| Edit distance | Insert/delete/replace | Three-way min on mismatch | Edit distance tutorial |
See also longest increasing subsequence for subsequence on numeric arrays (1D, not 2D string DP).
Space Optimization
LCS depends only on the previous row — compress to O(n) space:
2D: dp[i][j] from row i-1 and column j-1
1D: prev[j] and curr[j]; iterate i = 1..m, j = 1..n
match: curr[j] = prev[j-1] + 1
else: curr[j] = max(prev[j], curr[j-1])
LPS: interval DP needs full table (or O(n²) with careful ordering)
— harder to optimize to O(n) without losing reconstruction
More Examples
Example 1: LCS → min deletions (LeetCode 583)
| word1 | word2 | LCS | Min deletions | Calc |
|---|---|---|---|---|
| sea | eat | 2 ("ea") | 2 | (3−2)+(3−2) = 1+1 deletions |
Min deletions to make equal = (m − LCS) + (n − LCS) = m + n − 2·LCS.
Example 2: No common subsequence
| text1 | text2 | LCS length | LPS of "abcba" |
|---|---|---|---|
| abc | def | 0 | 5 ("abcba") |
Example 3: Complexity summary
| Problem | Time | Space | Notes |
|---|---|---|---|
| LCS | O(m × n) | O(m × n) / O(n) | Classic 2D string DP |
| LPS | O(n²) | O(n²) | Interval DP on one string |
| Distinct subsequences | O(m × n) | O(n) possible | Counting variant |
| Brute force | O(2^m) | O(m) | Generate all subsequences — avoid |
Approach Comparison
| Approach | Time | Space | When to use |
|---|---|---|---|
| 2D bottom-up DP | O(m × n) | O(m × n) | Whiteboard; reconstruct subsequence |
| 1D rolling array (LCS) | O(m × n) | O(n) | Space-optimized final code |
| Top-down memo | O(m × n) | O(m × n) + stack | Quick recursive draft |
| Interval DP (LPS) | O(n²) | O(n²) | Substrings of one string |
Real-Life Applications
| Domain | Real scenario | Subsequence mapping | Variant |
|---|---|---|---|
| Bioinformatics | DNA/protein sequence alignment | Find conserved regions across genomes | LCS / edit distance |
| Version control | Git diff / merge — common lines kept | Two file versions → longest unchanged subsequence | LCS |
| Plagiarism detection | Similar document passages | Long common subsequence of token streams | LCS / substring |
| Spell check / NLP | Measure similarity of sentences | Word-level LCS as similarity score | LCS |
| Data compression | Palindrome structure in repeated data | Longest palindromic patterns | LPS |
| Screen readers | Palindrome detection in tokens | Symmetric text patterns | LPS |
| Database sync | Minimal edits to reconcile two logs | LCS → what to keep; rest insert/delete | Delete ops / SCS |
| Competitive programming | Count ways string T appears in S | Distinct subsequences DP | LeetCode 115 |
Interview Tips
- Clarify: subsequence (gaps OK) vs substring (contiguous) vs palindrome constraint.
- Define
dp[i][j]in words before writing loops — prefix vs substring range. - Draw the small LCS table for "ace" vs "abcde" on the whiteboard.
- For LPS: fill
ifrom n−1 down to 0; base casedp[i][i]=1. - Follow-ups: reconstruct LCS (backtrack from dp[m][n]); O(n) space for LCS.
- Connect delete-operation (583) to LCS: deletions = m + n − 2·LCS.
- Do not confuse LCS with edit distance — replace costs extra.
- State complexity O(m × n) time explicitly.
Key Takeaway
LCS: on match take diagonal + 1; on mismatch take max of top/left. LPS: on match take inner interval + 2; fill backwards over substrings. Both are 2D string DP — master the table trace and you unlock delete-op, supersequence, and distinct-subsequence variants.
Next up: Edit distance · Longest increasing subsequence · Word break