Find the Shortest Superstring
HardPrompt
Given an array of strings words, return the smallest string that contains each string in words as a substring. If there are multiple valid strings of the smallest length, return any of them.
You may assume that no string in words is a substring of another string in words.
Example 1:
Input: words = ["alex","loves","leetcode"]
Output: "alexlovesleetcode"
Explanation: All permutations of "alex","loves","leetcode" would also be accepted.Example 2:
Input: words = ["catg","ctaagt","gcta","ttca","atgcatc"]
Output: "gctaagttcatgcatc"
Constraints:
1 <= words.length <= 121 <= words[i].length <= 20words[i]consists of lowercase English letters.- All the strings of
wordsare unique.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach exhaustively explores every possible ordering of the input strings. For each ordering, which is a permutation of the words array, it constructs the corresponding superstring by merging adjacent strings with the maximum possible overlap. It maintains a record of the shortest superstring found throughout this process and returns it as the final answer. While conceptually straightforward, its factorial time complexity makes it impractical for the constraints of this problem.
Algorithm
- Define a recursive function, say
permute(wordList, start), that generates all permutations of the inputwords. - The base case for the recursion is when
startreaches the end of the list. At this point, a full permutation has been generated. - For each complete permutation, construct the corresponding superstring. Start with the first word, then for each subsequent word, find the maximum overlap with the previous word and append the non-overlapping part.
- Keep track of the shortest superstring found so far across all permutations.
- The main function initializes the process by calling the recursive function with
start = 0. - A helper function
calculateOverlap(s1, s2)is used to find the length of the longest suffix ofs1that is also a prefix ofs2.
Walkthrough
The core idea is to try every single arrangement of the words and see which one produces the shortest combined string. We can implement this using a recursive function that generates all permutations of the words array.
For each permutation, we build the superstring from left to right. We start with the first word in the permutation. Then, we take the second word, find how much its beginning overlaps with the end of our current superstring, and append the rest of the second word. We repeat this process until all words in the permutation have been added.
After constructing a superstring for a permutation, we compare its length with the shortest one we've found so far. If the new one is shorter, we update our answer. After checking all n! permutations, we will have found the optimal solution.
For example, if words = ["a", "b", "c"], we would test permutations like ("a", "b", "c") -> "abc", ("a", "c", "b") -> "acb", etc., and find the shortest result.
import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.List; class Solution { private String shortestSuperstring = ""; public String shortestSuperstring(String[] words) { int n = words.length; if (n == 0) return ""; if (n == 1) return words[0]; List<String> wordList = new ArrayList<>(Arrays.asList(words)); permute(wordList, 0); return shortestSuperstring; } private void permute(List<String> words, int start) { if (start == words.size()) { StringBuilder currentSuperstring = new StringBuilder(words.get(0)); for (int i = 1; i < words.size(); i++) { String prev = words.get(i - 1); String curr = words.get(i); int overlap = calculateOverlap(prev, curr); currentSuperstring.append(curr.substring(overlap)); } if (shortestSuperstring.isEmpty() || currentSuperstring.length() < shortestSuperstring.length()) { shortestSuperstring = currentSuperstring.toString(); } return; } for (int i = start; i < words.size(); i++) { Collections.swap(words, start, i); permute(words, start + 1); Collections.swap(words, start, i); // backtrack } } private int calculateOverlap(String s1, String s2) { int maxOverlap = 0; for (int k = Math.min(s1.length(), s2.length()); k > 0; k--) { if (s1.endsWith(s2.substring(0, k))) { maxOverlap = k; break; } } return maxOverlap; }}Complexity
Time
O(n! * n * L^2), where `n` is the number of words and `L` is the maximum length. There are `n!` permutations. For each, we iterate through `n-1` pairs of words to build the superstring. Merging involves calculating overlap, which takes O(L^2), and string concatenation.
Space
O(n * L), where `n` is the number of words and `L` is the maximum length of a word. This space is used for the recursion stack and to store the generated superstrings.
Trade-offs
Pros
Simple to understand and implement.
Guaranteed to find the optimal solution by checking every possibility.
Cons
Extremely high time complexity, making it infeasible for
nlarger than about 8-10.Redundant computations, as overlaps between pairs of strings are calculated multiple times across different permutations.
This problem can be framed as the Traveling Salesperson Problem (TSP), an NP-hard problem. Each string represents a 'city', and the 'distance' or cost of traveling from city i to city j is the length added when appending words[j] after words[i], which is words[j].length() - overlap(words[i], words[j]). The goal is to find a path that visits all cities exactly once with minimum total cost. For the given constraint of n <= 12, this TSP variant can be solved optimally using dynamic programming with bitmasking.
Algorithm
- Step 1: Pre-computation. Calculate and store the overlap length for every ordered pair of strings
(words[i], words[j])in a 2D array,overlaps[i][j]. - Step 2: DP State. Define a 2D DP table,
dp[mask][i], to store the length of the shortest superstring for the subset of words represented bymask, ending withwords[i]. Also, define aparent[mask][i]table to reconstruct the path. - Step 3: Base Case. Initialize the DP table for subsets of size one:
dp[1 << i][i] = words[i].length()for allifrom 0 ton-1. - Step 4: DP Transition. Iterate through masks from 1 to
(1 << n) - 1. For eachmask, and for each wordiin the subsetmask, find the minimum path length by trying every other wordjin the subset as the previous word. The transition is:dp[mask][i] = min(dp[mask ^ (1 << i)][j] + words[i].length() - overlaps[j][i]). - Step 5: Find Final Result. After filling the table, find the minimum value in
dp[(1 << n) - 1][i]for alli. This gives the length of the shortest superstring and the index of its last word. - Step 6: Reconstruct Path. Backtrack using the
parenttable from the final state to find the optimal permutation of words. - Step 7: Build Superstring. Construct the final superstring using the optimal permutation and the pre-calculated overlaps.
Walkthrough
This approach provides an efficient way to solve the problem by avoiding the redundant computations of the brute-force method. It uses dynamic programming to build the solution from smaller subproblems to larger ones.
-
Pre-computation of Overlaps: We first create an
n x nmatrix to store the length of the maximum overlap for every pair of words(words[i], words[j]). This prevents recalculating these values repeatedly. -
DP State: The state of our DP is
dp[mask][i], representing the length of the shortest superstring that includes the set of words indicated by the bitmaskmask, and ends with the wordwords[i]. -
DP Transitions: We build up the
dptable by increasing the size of the subset of words. To computedp[mask][i], we look at all possible previous wordswords[j]that could have come beforewords[i]. The previous state would be for the subsetmaskwithoutwords[i], ending inwords[j]. We transition fromdp[prev_mask][j]todp[mask][i]by adding the cost of appendingwords[i]afterwords[j]. We take the minimum over all possiblej. -
Path Reconstruction: Simply knowing the minimum length is not enough; we need the actual string. We use an auxiliary
parent[mask][i]table to store which previous wordjgave the optimal solution fordp[mask][i]. After the DP table is filled, we find the end of the optimal path (theithat minimizesdp[(1<<n)-1][i]) and backtrack using theparenttable to reconstruct the sequence of words. -
Final Construction: With the optimal sequence of words, we can easily construct the shortest superstring by joining them, taking care to use the overlaps.
import java.util.Arrays; class Solution { public String shortestSuperstring(String[] words) { int n = words.length; if (n == 0) return ""; if (n == 1) return words[0]; int[][] overlaps = new int[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (i == j) continue; overlaps[i][j] = calculateOverlap(words[i], words[j]); } } int[][] dp = new int[1 << n][n]; int[][] parent = new int[1 << n][n]; for (int[] row : dp) { Arrays.fill(row, Integer.MAX_VALUE / 2); } for (int i = 0; i < n; i++) { dp[1 << i][i] = words[i].length(); } for (int mask = 1; mask < (1 << n); mask++) { for (int i = 0; i < n; i++) { if ((mask & (1 << i)) > 0) { int prevMask = mask ^ (1 << i); if (prevMask == 0) continue; for (int j = 0; j < n; j++) { if ((prevMask & (1 << j)) > 0) { int len = dp[prevMask][j] + words[i].length() - overlaps[j][i]; if (len < dp[mask][i]) { dp[mask][i] = len; parent[mask][i] = j; } } } } } } int minLen = Integer.MAX_VALUE; int lastIndex = -1; int finalMask = (1 << n) - 1; for (int i = 0; i < n; i++) { if (dp[finalMask][i] < minLen) { minLen = dp[finalMask][i]; lastIndex = i; } } int[] path = new int[n]; int currentMask = finalMask; for (int i = n - 1; i >= 0; i--) { path[i] = lastIndex; int prevIndex = parent[currentMask][lastIndex]; currentMask ^= (1 << lastIndex); lastIndex = prevIndex; } StringBuilder result = new StringBuilder(words[path[0]]); for (int i = 1; i < n; i++) { int prev = path[i - 1]; int curr = path[i]; result.append(words[curr].substring(overlaps[prev][curr])); } return result.toString(); } private int calculateOverlap(String s1, String s2) { int maxOverlap = 0; for (int k = Math.min(s1.length(), s2.length()); k > 0; k--) { if (s1.endsWith(s2.substring(0, k))) { maxOverlap = k; break; } } return maxOverlap; }}Complexity
Time
O(n^2 * L^2 + 2^n * n^2), where `n` is the number of words and `L` is their max length. `O(n^2 * L^2)` is for pre-calculating all pairwise overlaps. `O(2^n * n^2)` is for filling the DP table.
Space
O(2^n * n), where `n` is the number of words. This space is dominated by the `dp` and `parent` tables.
Trade-offs
Pros
Guaranteed to find the optimal solution.
Significantly more efficient than brute-force for the given constraints.
It's a standard and powerful technique for solving TSP-like problems on small datasets.
Cons
The exponential time and space complexity with respect to
nmakes it infeasible for largen(e.g.,n > 20).The implementation is significantly more complex than the brute-force approach.
Solutions
Solution
class Solution {public String shortestSuperstring(String[] words) { int n = words.length; int[][] g = new int[n][n]; for (int i = 0; i < n; ++i) { String a = words[i]; for (int j = 0; j < n; ++j) { String b = words[j]; if (i != j) { for (int k = Math.min(a.length(), b.length()); k > 0; --k) { if (a.substring(a.length() - k).equals(b.substring(0, k))) { g[i][j] = k; break; } } } } } int[][] dp = new int[1 << n][n]; int[][] p = new int[1 << n][n]; for (int i = 0; i < 1 << n; ++i) { Arrays.fill(p[i], -1); for (int j = 0; j < n; ++j) { if (((i >> j) & 1) == 1) { int pi = i ^ (1 << j); for (int k = 0; k < n; ++k) { if (((pi >> k) & 1) == 1) { int v = dp[pi][k] + g[k][j]; if (v > dp[i][j]) { dp[i][j] = v; p[i][j] = k; } } } } } } int j = 0; for (int i = 0; i < n; ++i) { if (dp[(1 << n) - 1][i] > dp[(1 << n) - 1][j]) { j = i; } } List<Integer> arr = new ArrayList<>(); arr.add(j); for (int i = (1 << n) - 1; p[i][j] != -1;) { int k = i; i ^= (1 << j); j = p[k][j]; arr.add(j); } Set<Integer> vis = new HashSet<>(arr); for (int i = 0; i < n; ++i) { if (!vis.contains(i)) { arr.add(i); } } Collections.reverse(arr); StringBuilder ans = new StringBuilder(words[arr.get(0)]); for (int i = 1; i < n; ++i) { int k = g[arr.get(i - 1)][arr.get(i)]; ans.append(words[arr.get(i)].substring(k)); } return ans.toString(); }}class Solution {public: string shortestSuperstring(vector<string> &words) { int n = words.size(); vector<vector<int>> g(n, vector<int>(n)); for (int i = 0; i < n; ++i) { auto a = words[i]; for (int j = 0; j < n; ++j) { auto b = words[j]; if (i != j) { for (int k = min(a.size(), b.size()); k > 0; --k) { if (a.substr(a.size() - k) == b.substr(0, k)) { g[i][j] = k; break; } } } } } vector<vector<int>> dp(1 << n, vector<int>(n)); vector<vector<int>> p(1 << n, vector<int>(n, -1)); for (int i = 0; i < 1 << n; ++i) { for (int j = 0; j < n; ++j) { if ((i >> j) & 1) { int pi = i ^ (1 << j); for (int k = 0; k < n; ++k) { if ((pi >> k) & 1) { int v = dp[pi][k] + g[k][j]; if (v > dp[i][j]) { dp[i][j] = v; p[i][j] = k; } } } } } } int j = 0; for (int i = 0; i < n; ++i) { if (dp[(1 << n) - 1][i] > dp[(1 << n) - 1][j]) { j = i; } } vector<int> arr = {j}; for (int i = (1 << n) - 1; p[i][j] != -1;) { int k = i; i ^= (1 << j); j = p[k][j]; arr.push_back(j); } unordered_set<int> vis(arr.begin(), arr.end()); for (int i = 0; i < n; ++i) { if (!vis.count(i)) { arr.push_back(i); } } reverse(arr.begin(), arr.end()); string ans = words[arr[0]]; for (int i = 1; i < n; ++i) { int k = g[arr[i - 1]][arr[i]]; ans += words[arr[i]].substr(k); } return ans; }};class Solution: def shortestSuperstring(self, words: List[str]) -> str: n = len(words) g = [[0] * n for _ in range(n)] for i, a in enumerate(words): for j, b in enumerate(words): if i != j: for k in range(min(len(a), len(b)), 0, - 1): if a[- k:] == b[: k]: g[i][j] = k break dp = [[0] * n for _ in range(1 << n)] p = [[- 1] * n for _ in range(1 << n)] for i in range(1 << n): for j in range(n): if (i >> j) & 1: pi = i ^ (1 << j) for k in range(n): if (pi >> k) & 1: v = dp[pi][k] + g[k][j] if v > dp[i][j]: dp[i][j] = v p[i][j] = k j = 0 for i in range(n): if dp[- 1][i] > dp[- 1][j]: j = i arr = [j] i = (1 << n) - 1 while p[i][j] != - 1: i, j = i ^ (1 << j), p[i][j] arr . append(j) arr = arr[:: - 1] vis = set(arr) arr . extend([j for j in range(n) if j not in vis]) ans = [words[arr[0]]] + [words[j][g[i][j]:] for i, j in pairwise(arr)] return '' . join(ans)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.