Topics › Dynamic Programming › DP on Strings
Shortest common supersequence
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given two strings s1 and s2, return the shortest string that contains both of them as subsequences. If there are several shortest strings, any one of them is accepted.
Constraints (LeetCode 1092): 1 ≤ n, m ≤ 1000, lowercase English letters. The Java methods also accept empty strings.
Example 1
s1 = "abac", s2 = "cab"
answer = "cabac" // drop the first 'c' -> "abac"; drop the last "ac" -> "cab"
Example 2
s1 = "brute", s2 = "groot"
answer = "bgruoote" // length 8 = 5 + 5 - 2, the shared "rt" is written once
Key insight: write the shared part once
Concatenating the strings always works but wastes space. Each character of a supersequence serves s1, s2, or both. The characters that serve both appear in the same order in both strings, so they form a common subsequence. The more of them there are, the shorter the result:
length = n + m - (number of shared characters) ≥ n + m - LCS
and the bound is reached by sharing a Longest common subsequence. For Example 2, the LCS of "brute" and "groot" is "rt", so the answer has length 5 + 5 - 2 = 8.
To build the string itself, merge the two strings along an LCS: write every LCS character once, and before each one, write the characters of s1 and s2 that lie between it and the previous LCS character.
Approach 1: Brute force, recursion returning strings
Intuition. Work on prefixes s1[0..i) and s2[0..j) and decide the last character of the supersequence:
- If one prefix is empty, the answer is the other prefix.
- If the last characters match, one copy serves both: solve
(i - 1, j - 1)and append it. - Otherwise the result ends with either
s1[i - 1]ors2[j - 1]. Try both and keep the shorter (on a tie, the one ending withs2[j - 1]). That tie rule is chosen to match Approach 2’s walk, so both methods return the same string.
Algorithm.
- If
i == 0returns2[0..j); ifj == 0returns1[0..i). - On a match return
build(i - 1, j - 1) + s1[i - 1]. - Otherwise compare
build(i - 1, j) + s1[i - 1]withbuild(i, j - 1) + s2[j - 1]and return the shorter.
static String bruteForce(String s1, String s2) {
return build(s1.length(), s2.length(), s1, s2);
}
/** A shortest common supersequence of s1[0..i) and s2[0..j). Ties go to "s2's last character goes last". */
static String build(int i, int j, String s1, String s2) {
if (i == 0) return s2.substring(0, j); // nothing left of s1: the rest of s2 is needed as is
if (j == 0) return s1.substring(0, i);
if (s1.charAt(i - 1) == s2.charAt(j - 1)) return build(i - 1, j - 1, s1, s2) + s1.charAt(i - 1); // one copy serves both
String up = build(i - 1, j, s1, s2) + s1.charAt(i - 1); // s1's last character ends the answer
String left = build(i, j - 1, s1, s2) + s2.charAt(j - 1);// s2's last character ends the answer
return up.length() < left.length() ? up : left;
}
Complexity. Time exponential, about O(2^(n + m)) calls, each building a string of length up to n + m. Space O(n + m) stack plus the strings in flight. Useful only to check the fast method on small inputs.
Approach 2: Optimal, LCS table and walk back
Intuition. Build the LCS length table, then start from (n, m) and retrace the decisions, writing characters as you go (back to front):
s1[i - 1] == s2[j - 1]: an LCS character. Write it once and move diagonally.- Otherwise, move toward the neighbour with the larger LCS value, because that is where the LCS continues, and write the character you step over: moving up skips
s1[i - 1], so write it; moving left skipss2[j - 1], so write that. On a tie either direction is fine; the code moves left. - When one index reaches 0, the other string still has characters left. They are not shared with anything, so write them all.
Compared with printing the LCS, the only differences are that unmatched characters are written instead of skipped, and the leftover prefix is appended at the end. Reverse the builder once at the end.
Algorithm.
- Fill
dp[n + 1][m + 1]with LCS lengths. - Walk from
(n, m)whilei > 0andj > 0, appending as described. - Append the rest of
s1[0..i)ands2[0..j)(one of them is empty). - Reverse and return.
static String optimal(String s1, String s2) {
int n = s1.length(), m = s2.length();
int[][] dp = new int[n + 1][m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s1.charAt(i - 1) == s2.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]);
}
}
StringBuilder sb = new StringBuilder(); // built backwards, reversed at the end
int i = n, j = m;
while (i > 0 && j > 0) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) { // an LCS character: write it once for both strings
sb.append(s1.charAt(i - 1));
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
sb.append(s1.charAt(i - 1)); // s1's character is not shared: write it alone
i--;
} else {
sb.append(s2.charAt(j - 1));
j--;
}
}
while (i > 0) sb.append(s1.charAt(--i)); // whatever is left of either string
while (j > 0) sb.append(s2.charAt(--j));
return sb.reverse().toString();
}
Dry run on Example 2, s1 = "brute" (rows), s2 = "groot" (columns). The LCS table:
| ”” | g | r | o | o | t | |
|---|---|---|---|---|---|---|
| ”” | 0 | 0 | 0 | 0 | 0 | 0 |
| b | 0 | 0 | 0 | 0 | 0 | 0 |
| r | 0 | 0 | 1 | 1 | 1 | 1 |
| u | 0 | 0 | 1 | 1 | 1 | 1 |
| t | 0 | 0 | 1 | 1 | 1 | 2 |
| e | 0 | 0 | 1 | 1 | 1 | 2 |
| at (i, j) | compare | move | written |
|---|---|---|---|
| (5, 5) | e ≠ t, up 2 > left 1 | up | e |
| (4, 5) | t = t | diagonal | t |
| (3, 4) | u ≠ o, tie 1 = 1 | left | o |
| (3, 3) | u ≠ o, tie 1 = 1 | left | o |
| (3, 2) | u ≠ r, up 1 > left 0 | up | u |
| (2, 2) | r = r | diagonal | r |
| (1, 1) | b ≠ g, tie 0 = 0 | left | g |
| (1, 0) | s2 used up |
leftover | b |
Written back to front: e t o o u r g b. Reversed: "bgruoote", length 8.
Complexity. Time O(n · m) for the table plus O(n + m) for the walk. Space O(n · m) for the table.
Length only
If only the length is needed, there is no walk, so two LCS rows suffice and the answer is n + m - LCS. The tests use this method to confirm the length of every string the other two return.
static int shortestLength(String s1, String s2) {
int n = s1.length(), m = s2.length();
int[] prev = new int[m + 1], cur = new int[m + 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) cur[j] = 1 + prev[j - 1];
else cur[j] = Math.max(prev[j], cur[j - 1]);
}
int[] tmp = prev; prev = cur; cur = tmp;
}
return n + m - prev[m];
}
Complexity. Time O(n · m), space O(m). Recovering the string itself in linear space is possible with Hirschberg’s divide and conquer, shown in Print Longest Common Subsequence - (DP - 26).
Edge cases and pitfalls
- Forgetting the leftovers. When the loop stops because
j == 0, the characterss1[0..i)have not been written yet. Dropping them is the most common bug;("ab", "b")would return"b"instead of"ab". - Writing the wrong character. Moving up means
s1[i - 1]is not shared, so it is the one to write. Mixing up the two cases still produces a string of the right length that is not a supersequence. - Reversal. The walk produces the answer back to front; reverse once at the end rather than inserting at the front of a string in a loop.
- Several answers. For
("abc", "def")every interleaving of the two strings, 20 in all, is a valid answer. The tests therefore check “is a supersequence of both and has lengthn + m - LCS”, and compare exact strings only when the answer is unique. - Identical or nested strings. If
s2is a subsequence ofs1, the answer iss1itself. - Testing. Besides the fixed cases, 200 seeded random pairs over
{a, b, c}are checked for all three methods.
Related problems
- Longest common subsequence gives the table and the length formula.
- Print Longest Common Subsequence - (DP - 26) uses the same walk but skips unmatched characters.
- Minimum insertions or deletions to convert string A to B counts
n + m - 2 · LCSoperations with the same reasoning. - Merge two Sorted Lists is the simplest form of merging two sequences while keeping each one’s order.