Shortest Common Supersequence — LCS + Reconstruction for Hard DP Interviews
Advertisement
Problem Statement
Given two strings
str1andstr2, return the shortest string that has bothstr1andstr2as subsequences. If there are multiple valid strings, return any of them.
Constraints:
1 <= str1.length, str2.length <= 1000str1andstr2consist of lowercase English letters
Example 1:
Input: str1 = "abac", str2 = "cab"
Output: "cabac"
Explanation:
str1 = "abac" is a subsequence of "cabac" (remove c, b) -> abac
str2 = "cab" is a subsequence of "cabac" (remove a, c) -> cab
"cabac" is the shortest such string (length 5).Example 2:
Input: str1 = "aaaaaaaa", str2 = "aaaaaaaa"
Output: "aaaaaaaa"
Explanation: Both strings are identical — the supersequence is the string itself.Example 3:
Input: str1 = "abc", str2 = "xyz"
Output: "axbycz" (or any other valid SCS of length 6)
Explanation: No characters in common — all must appear once each.Why This Problem Matters
Shortest Common Supersequence (SCS) is a hard-level problem that builds directly on LCS. It tests two skills simultaneously:
- Computing the LCS using 2D DP (LC 1143 pattern)
- Reconstructing the actual SCS string by backtracking the DP table
The connection is elegant: SCS length = len(str1) + len(str2) - LCS(str1, str2). Characters in the LCS appear once; all other characters from both strings appear separately. The reconstruction step — which characters to include and in which order — is the hard part that separates candidates who memorize formulas from those who truly understand 2D DP structure.
Google and Amazon include this problem to test DP reconstruction skills that also appear in Edit Distance path recovery, sequence alignment in bioinformatics, and diff algorithms.
The Core Insight
Step 1: Compute the LCS length table.
Use the standard LCS recurrence on a (m+1) x (n+1) DP table:
dp[i][j] = dp[i-1][j-1] + 1ifstr1[i-1] == str2[j-1]dp[i][j] = max(dp[i-1][j], dp[i][j-1])otherwise
Step 2: Reconstruct the SCS by backtracking the LCS table.
Start at (m, n) and move toward (0, 0):
- If
str1[i-1] == str2[j-1]: the character is part of the LCS — include it once and move diagonally(i-1, j-1). - If
dp[i-1][j] > dp[i][j-1]: came from above — includestr1[i-1]and move up(i-1, j). - Otherwise: came from left — include
str2[j-1]and move left(i, j-1).
After reaching row 0 or column 0, append the remaining characters from whichever string still has characters left.
Reverse the collected characters (built backward during backtracking).
Length formula: SCS_length = m + n - LCS_length
Building the DP Solution
Step 1 — Compute LCS table:
def lcs_table(str1, str2):
m, n = len(str1), len(str2)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if str1[i-1] == str2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dpStep 2 — Reconstruct SCS from LCS table:
def reconstruct(str1, str2, dp):
i, j = len(str1), len(str2)
result = []
while i > 0 and j > 0:
if str1[i-1] == str2[j-1]:
result.append(str1[i-1]) # LCS character — include once
i -= 1
j -= 1
elif dp[i-1][j] > dp[i][j-1]:
result.append(str1[i-1]) # str1 character not in LCS
i -= 1
else:
result.append(str2[j-1]) # str2 character not in LCS
j -= 1
# Append remaining characters
while i > 0:
result.append(str1[i-1])
i -= 1
while j > 0:
result.append(str2[j-1])
j -= 1
return ''.join(reversed(result))Visual Dry Run
Input: str1 = "abac", str2 = "cab"
LCS DP table:
| "" | c | a | b | |
|---|---|---|---|---|
| "" | 0 | 0 | 0 | 0 |
| a | 0 | 0 | 1 | 1 |
| b | 0 | 0 | 1 | 2 |
| a | 0 | 0 | 1 | 2 |
| c | 0 | 1 | 1 | 2 |
LCS length = dp[4][3] = 2. LCS = "ab".
Backtracking trace (start at i=4, j=3):
| i | j | action | char added |
|---|---|---|---|
| 4 | 3 | str1[3]='c' == str2[2]='b'? No. dp[3][3]=2 == dp[4][2]=1? No → take str2[2]='b' | 'b' |
| 4 | 2 | str1[3]='c' == str2[1]='a'? No. dp[3][2]=1 == dp[4][1]=1? tie → take str2[1]='a' | 'a' |
| 4 | 1 | str1[3]='c' == str2[0]='c'? Yes → LCS char, take 'c' | 'c' |
| 3 | 0 | j=0: append remaining str1[0..2] = 'a','b','a' reversed | 'a','b','a' |
Result (reversed): 'a','b','a','c','a','b' reversed = "cabac". Length 5. Correct.
Full SCS reconstruction table:
| "" | c | a | b | |
|---|---|---|---|---|
| "" | "" | c | ca | cab |
| a | a | ac | ca | cab |
| b | ab | acb | cab | cab |
| a | aba | acba | caba | caba |
| c | abac | cabac | cabac | cabac |
Optimized Solution
Python
class Solution:
def shortestCommonSupersequence(self, str1: str, str2: str) -> str:
m, n = len(str1), len(str2)
# Step 1: Build full LCS DP table (needed for reconstruction)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i - 1] == str2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# Step 2: Reconstruct SCS by backtracking from (m, n) to (0, 0)
result = []
i, j = m, n
while i > 0 and j > 0:
if str1[i - 1] == str2[j - 1]:
result.append(str1[i - 1]) # LCS character — include once
i -= 1
j -= 1
elif dp[i - 1][j] > dp[i][j - 1]:
result.append(str1[i - 1]) # str1-only character
i -= 1
else:
result.append(str2[j - 1]) # str2-only character
j -= 1
# Append any remaining characters from str1 or str2
while i > 0:
result.append(str1[i - 1])
i -= 1
while j > 0:
result.append(str2[j - 1])
j -= 1
return ''.join(reversed(result))JavaScript
var shortestCommonSupersequence = function(str1, str2) {
const m = str1.length;
const n = str2.length;
// Build LCS DP table
const dp = 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 (str1[i - 1] === str2[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]);
}
}
}
// Reconstruct SCS
let i = m, j = n;
const result = [];
while (i > 0 && j > 0) {
if (str1[i - 1] === str2[j - 1]) {
result.push(str1[i - 1]);
i--; j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
result.push(str1[i - 1]);
i--;
} else {
result.push(str2[j - 1]);
j--;
}
}
while (i > 0) { result.push(str1[i - 1]); i--; }
while (j > 0) { result.push(str2[j - 1]); j--; }
return result.reverse().join('');
};Complexity Analysis
| Aspect | Complexity | Notes |
|---|---|---|
| Time (LCS table) | O(m * n) | Fill (m+1) x (n+1) table |
| Time (reconstruction) | O(m + n) | Single backtracking pass |
| Space (DP table) | O(m * n) | Full table needed for reconstruction |
| Space (result) | O(m + n) | SCS length is at most m+n |
Note: Space cannot be reduced to O(n) for this problem because reconstruction requires the full 2D table.
Common Mistakes
1. Computing SCS length only and claiming it is the answer.
The problem asks for the actual SCS string, not just its length. Many candidates compute m + n - LCS_length and stop. You must reconstruct the string via backtracking.
2. Including LCS characters twice.
When backtracking and both characters at str1[i-1] and str2[j-1] match, include the character once and advance both i and j. Including it from both strings doubles it in the SCS — a very common error.
3. Incorrect tie-breaking in backtracking.
When dp[i-1][j] == dp[i][j-1], either direction is valid. Consistently choosing one direction (e.g., always prefer str1) prevents inconsistent results. The problem says any valid SCS is acceptable.
4. Forgetting to append remaining characters after the while loop.
After the main while i > 0 and j > 0 loop, one string may still have characters remaining. Always append them: while i > 0: append str1[i-1], i-- and same for j.
5. Reversing incorrectly or not at all.
Characters are collected from (m, n) back to (0, 0) — in reverse order. The result must be reversed at the end. Forgetting this produces the SCS in backward order.
Interview Tips
- State the LCS connection explicitly: "The SCS length is
m + n - LCS_lengthbecause LCS characters appear once; all others appear individually." - Draw the DP table and show the backtracking path. Interviewers want to see that you can reason about the diagonal/up/left transitions.
- Explain the backtracking rule precisely: "On character match, include once and move diagonally. On mismatch, include the character from whichever string leads to a larger LCS value — that's the string we're 'consuming' at this step."
- Mention you cannot reduce space for this problem: "Unlike LCS length where 1D rolling works, reconstruction requires the full 2D table."
- For the length-only variant: "If only the SCS length is needed,
m + n - LCS(str1, str2)with the rolling-array LCS gives O(n) space."
Follow-up Questions
Q: What if you only need the length of the SCS?
SCS_length = len(str1) + len(str2) - longestCommonSubsequence(str1, str2). Use the space-optimized rolling-array LCS (O(n) space).
Q: How does SCS relate to Edit Distance?
Edit Distance (inserts + deletes only, no replacements) = m + n - 2 * LCS. SCS length = m + n - LCS. Both are derived from LCS; Edit Distance penalizes the mismatch gap from both sides.
Q: What is the SCS of 3 strings?
Extend the LCS DP to 3D: dp[i][j][k] = LCS of the three prefixes. SCS length = m + n + p - (pairwise corrections). Reconstruction follows the same logic across 3 dimensions.
Q: Is the SCS unique? Not necessarily. Multiple valid SCS strings of the same minimum length may exist. The problem only asks for any one of them, which makes tie-breaking in backtracking a free choice.
Key Takeaways
- SCS = characters from the LCS (included once) + all non-LCS characters from both strings (included separately). Length =
m + n - LCS_length. - Reconstruction backtracks the LCS table: diagonal on character match (include once), up (include
str1[i-1]), left (includestr2[j-1]). - After the main loop, always append remaining characters from whichever string is not yet exhausted.
- Always reverse the collected result — backtracking goes from end to start.
- The full 2D LCS table is required for reconstruction — space cannot be compressed to 1D for this problem.
- This problem is a hard-level benchmark that combines LCS table computation with reconstruction — mastering it makes Edit Distance path recovery and sequence alignment problems trivial by comparison.
Advertisement