Shortest Common Supersequence
HardPrompt
Given two strings str1 and str2, return the shortest string that has both str1 and str2 as subsequences. If there are multiple valid strings, return any of them.
A string s is a subsequence of string t if deleting some number of characters from t (possibly 0) results in the string s.
Example 1:
Input: str1 = "abac", str2 = "cab"
Output: "cabac"
Explanation:
str1 = "abac" is a subsequence of "cabac" because we can delete the first "c".
str2 = "cab" is a subsequence of "cabac" because we can delete the last "ac".
The answer provided is the shortest such string that satisfies these properties.Example 2:
Input: str1 = "aaaaaaaa", str2 = "aaaaaaaa"
Output: "aaaaaaaa"
Constraints:
1 <= str1.length, str2.length <= 1000str1andstr2consist of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a straightforward recursive method to solve the problem. It breaks down the problem into smaller subproblems by considering the first characters of the two strings. While simple to conceptualize, it suffers from massive performance issues due to re-calculating solutions for the same pairs of substrings multiple times.
Algorithm
- Define a recursive function, say
solve(s1, s2), that takes two strings and returns their shortest common supersequence. - Base Case: If either string is empty, the SCS is simply the other non-empty string. Return it.
- Recursive Step:
- If the first characters of
s1ands2are the same (s1[0] == s2[0]): The common character can be used once. The result is this character prepended to the result of the recursive call on the rest of the strings:s1[0] + solve(s1.substring(1), s2.substring(1)). - If the first characters are different: We have two possibilities to form the supersequence.
- Include
s1[0]in the supersequence and find the SCS of the rest ofs1and all ofs2. Result:s1[0] + solve(s1.substring(1), s2). - Include
s2[0]in the supersequence and find the SCS of all ofs1and the rest ofs2. Result:s2[0] + solve(s1, s2.substring(1)).
- Include
- Compare the lengths of the two resulting strings from the above step and return the shorter one.
- If the first characters of
Walkthrough
The core idea is to build the shortest common supersequence (SCS) character by character from the beginning. At each step, we compare the first characters of the current substrings of str1 and str2.
If the characters match, we know this character must be in the SCS. We add it to our result and proceed to find the SCS for the remaining parts of both strings.
If the characters do not match, we have a choice. We can either take the character from str1 and find the SCS for the rest of str1 and the entirety of str2, or we can take the character from str2 and find the SCS for str1 and the rest of str2. To ensure the final supersequence is the shortest, we recursively explore both paths and choose the one that yields a shorter result.
This method naturally leads to a recursive structure but creates an explosion of function calls for the same subproblems, making it impractical for the given constraints.
// Note: This solution is for demonstration and will Time Limit Exceed.class Solution { public String shortestCommonSupersequence(String str1, String str2) { // Base case: if one string is empty, the other is the SCS. if (str1.isEmpty()) { return str2; } if (str2.isEmpty()) { return str1; } // If first characters are the same, they contribute one char to SCS. if (str1.charAt(0) == str2.charAt(0)) { return str1.charAt(0) + shortestCommonSupersequence(str1.substring(1), str2.substring(1)); } else { // If different, explore two paths and choose the shorter result. String res1 = str1.charAt(0) + shortestCommonSupersequence(str1.substring(1), str2); String res2 = str2.charAt(0) + shortestCommonSupersequence(str1, str2.substring(1)); if (res1.length() < res2.length()) { return res1; } else { return res2; } } }}Complexity
Time
O(2^(m+n)). In the worst-case scenario (when no characters match), each recursive call branches into two, leading to an exponential number of calls.
Space
O(m + n), where m and n are the lengths of the strings. This is due to the recursion depth and the storage of strings in each recursive call.
Trade-offs
Pros
Simple to understand and directly follows the problem's recursive definition.
Easy to implement without complex data structures.
Cons
Extremely inefficient due to a large number of redundant computations for the same subproblems.
Will result in a 'Time Limit Exceeded' (TLE) error for all but the smallest inputs due to its exponential time complexity.
This optimal approach uses dynamic programming. The key insight is that the shortest common supersequence is formed by taking all characters from both strings but counting the characters of their longest common subsequence (LCS) only once. The algorithm first computes the lengths of LCS for all prefixes of the strings and stores them in a DP table. Then, it backtracks through this table to construct the SCS string.
Algorithm
-
Step 1: Compute the Longest Common Subsequence (LCS) DP Table.
- Create a 2D array
dpof size(m+1) x (n+1), wherem = str1.length()andn = str2.length(). dp[i][j]will store the length of the LCS ofstr1.substring(0, i)andstr2.substring(0, j).- Fill the table using the standard LCS recurrence relation:
- If
str1[i-1] == str2[j-1], thendp[i][j] = 1 + dp[i-1][j-1]. - Otherwise,
dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
- If
- Create a 2D array
-
Step 2: Construct the SCS by Backtracking.
- Initialize two pointers,
i = mandj = n, and an emptyStringBuilderfor the result. - Traverse the
dptable backwards fromdp[m][n]:- If
str1[i-1] == str2[j-1]: This character is common. Append it to the result, and move diagonally up-left (i--,j--). - If
str1[i-1] != str2[j-1]: The characters are different. We must include both, but not at the same time. We check thedptable to see which character was excluded to form the LCS at the previous step.- If
dp[i-1][j] > dp[i][j-1], it means the path to the LCS came fromdp[i-1][j]. So,str1[i-1]is a non-common character that must be in the SCS. Appendstr1[i-1]and move up (i--). - Otherwise, the path came from
dp[i][j-1]. Append the non-common characterstr2[j-1]and move left (j--).
- If
- If
- After the loop, one of the pointers might still be greater than 0. Append the remaining part of the corresponding string.
- Finally, reverse the constructed string to get the correct SCS.
- Initialize two pointers,
Walkthrough
The problem of finding the shortest common supersequence is closely related to finding the longest common subsequence. The length of the SCS is given by len(str1) + len(str2) - len(LCS(str1, str2)). While we can compute the LCS string first, a more direct method is to use the DP table generated during the LCS length calculation to build the SCS.
First, we populate a dp[m+1][n+1] table where dp[i][j] stores the length of the LCS between str1[0...i-1] and str2[0...j-1]. This takes O(m*n) time.
Next, we reconstruct the SCS. We start from the end of both strings (i=m, j=n) and build the SCS string backwards.
- If
str1[i-1]andstr2[j-1]are the same, this character is part of the LCS. We add it once to our result and move to consider the prefixesstr1[0...i-2]andstr2[0...j-2]. - If they are different, we must include both characters in the SCS eventually. We look at our
dptable to decide which one to add now. The one that is not on the path to the longer LCS of the subproblems is added. For example, ifdp[i-1][j](LCS ofstr1prefix andstr2full) is greater thandp[i][j-1], it meansstr1[i-1]is unique at this step and must be added. We then move to the subproblem that gave the longer LCS.
This process continues until we've processed both strings completely. The resulting string is built in reverse, so a final reversal is needed.
class Solution { public String shortestCommonSupersequence(String str1, String str2) { int m = str1.length(); int n = str2.length(); // Step 1: Fill DP table for LCS lengths int[][] dp = new int[m + 1][n + 1]; for (int i = 0; i <= m; i++) { for (int j = 0; j <= n; j++) { if (i == 0 || j == 0) { dp[i][j] = 0; } else if (str1.charAt(i - 1) == str2.charAt(j - 1)) { dp[i][j] = 1 + dp[i - 1][j - 1]; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } // Step 2: Backtrack to build the SCS string StringBuilder scs = new StringBuilder(); int i = m, j = n; while (i > 0 && j > 0) { if (str1.charAt(i - 1) == str2.charAt(j - 1)) { scs.append(str1.charAt(i - 1)); i--; j--; } else if (dp[i - 1][j] > dp[i][j - 1]) { scs.append(str1.charAt(i - 1)); i--; } else { scs.append(str2.charAt(j - 1)); j--; } } // Append remaining characters from str1 or str2 while (i > 0) { scs.append(str1.charAt(i - 1)); i--; } while (j > 0) { scs.append(str2.charAt(j - 1)); j--; } return scs.reverse().toString(); }}Complexity
Time
O(m * n), where m and n are the lengths of the input strings. The dominant operation is filling the DP table.
Space
O(m * n) to store the 2D DP table required for backtracking.
Trade-offs
Pros
Guaranteed to find the optimal solution.
Efficient for the given constraints with a polynomial time complexity.
Cons
Requires extra space proportional to the product of the lengths of the two strings.
Solutions
Solution
class Solution {public String shortestCommonSupersequence(String str1, String str2) { int m = str1.length(), n = str2.length(); int[][] f = new int[m + 1][n + 1]; for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (str1.charAt(i - 1) == str2.charAt(j - 1)) { f[i][j] = f[i - 1][j - 1] + 1; } else { f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]); } } } int i = m, j = n; StringBuilder ans = new StringBuilder(); while (i > 0 || j > 0) { if (i == 0) { ans.append(str2.charAt(--j)); } else if (j == 0) { ans.append(str1.charAt(--i)); } else { if (f[i][j] == f[i - 1][j]) { ans.append(str1.charAt(--i)); } else if (f[i][j] == f[i][j - 1]) { ans.append(str2.charAt(--j)); } else { ans.append(str1.charAt(--i)); --j; } } } return ans.reverse().toString(); }}class Solution {public: string shortestCommonSupersequence(string str1, string str2) { int m = str1.size(), n = str2.size(); vector<vector<int>> f(m + 1, vector<int>(n + 1)); for (int i = 1; i <= m; ++i) { for (int j = 1; j <= n; ++j) { if (str1[i - 1] == str2[j - 1]) f[i][j] = f[i - 1][j - 1] + 1; else f[i][j] = max(f[i - 1][j], f[i][j - 1]); } } int i = m, j = n; string ans; while (i || j) { if (i == 0) ans += str2[--j]; else if (j == 0) ans += str1[--i]; else { if (f[i][j] == f[i - 1][j]) ans += str1[--i]; else if (f[i][j] == f[i][j - 1]) ans += str2[--j]; else ans += str1[--i], --j; } } reverse(ans.begin(), ans.end()); return ans; }};class Solution: def shortestCommonSupersequence(self, str1: str, str2: str) -> str: m, n = len(str1), len(str2) f = [[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]: f[i][j] = f[i - 1][j - 1] + 1 else: f[i][j] = max(f[i - 1][j], f[i][j - 1]) ans = [] i, j = m, n while i or j: if i == 0: j -= 1 ans . append(str2[j]) elif j == 0: i -= 1 ans . append(str1[i]) else: if f[i][j] == f[i - 1][j]: i -= 1 ans . append(str1[i]) elif f[i][j] == f[i][j - 1]: j -= 1 ans . append(str2[j]) else: i, j = i - 1, j - 1 ans . append(str1[i]) return '' . join(ans[:: - 1])Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.