2800. Shortest String That Contains Three Strings
Problem Description
You are given three strings a
, b
, and c
. Your task is to find the shortest possible string that contains all three strings as substrings.
A substring is a contiguous sequence of characters within a string. For example, "abc" is a substring of "xabcy" but not of "xabyc".
The goal is to create a string that:
- Contains string
a
as a substring - Contains string
b
as a substring - Contains string
c
as a substring - Has the minimum possible length
If there are multiple strings with the same minimum length that satisfy these conditions, return the lexicographically smallest one.
Lexicographical ordering means dictionary order - a string is lexicographically smaller than another string of the same length if at the first position where they differ, it has a character that comes earlier in the alphabet.
For example:
- If
a = "abc"
,b = "bca"
, andc = "aaa"
, one possible answer could be"aaabca"
which contains all three strings as substrings:"aaa"
is present starting at index 0"abc"
is present starting at index 2"bca"
is present starting at index 3
The solution involves trying different ways to merge the strings by finding overlaps between them and choosing the arrangement that produces the shortest result, with ties broken by lexicographical order.
Intuition
The key insight is that when we want to combine multiple strings into one while minimizing the total length, we should look for overlapping parts between strings. For instance, if we have "abc" and "bcd", instead of concatenating them as "abcbcd" (length 7), we can overlap them as "abcd" (length 4) since "bc" is common.
When dealing with three strings, the order in which we merge them matters. Consider merging "abc", "bca", and "cab". If we merge "abc" with "bca" first, we get "abca". Then merging with "cab" gives us "abcab". But if we started with "bca" and "cab", we'd get "bcab", and then merging with "abc" gives us "abcab" or potentially something different depending on overlaps.
Since there are only 3 strings, we can try all possible orderings - there are exactly 3! = 6
permutations. For each permutation, we merge the strings in that order and keep track of the shortest result.
The merging strategy for two strings works as follows:
- First check if one string is already contained in the other - if so, just return the longer string
- Otherwise, find the maximum overlap: check if the end of the first string matches the beginning of the second string
- Start with the largest possible overlap and work down to find a match
- If no overlap exists, simply concatenate the strings
By trying all 6 possible orderings and applying this merging strategy, we're guaranteed to find the optimal solution. Among all solutions with minimum length, we pick the lexicographically smallest one.
Learn more about Greedy patterns.
Solution Approach
The solution uses a helper function f(s, t)
to merge two strings optimally, and then applies this function across all permutations of the three input strings.
Helper Function f(s, t)
- Merging Two Strings:
-
Check containment: If string
s
is already contained int
, returnt
. Ift
is contained ins
, returns
. This handles cases where one string is a substring of another. -
Find maximum overlap: If neither contains the other, look for the longest overlap between the end of
s
and the beginning oft
:- Start with the minimum of the two string lengths (since overlap can't exceed the shorter string)
- Check progressively smaller overlaps from
min(m, n)
down to 1 - For each overlap length
i
, check if the lasti
characters ofs
match the firsti
characters oft
:s[-i:] == t[:i]
- If a match is found, return
s + t[i:]
(strings
followed by the non-overlapping part oft
)
-
No overlap: If no overlap exists, concatenate the strings:
s + t
Main Algorithm:
-
Generate all permutations: Use
permutations((a, b, c))
to generate all 6 possible orderings of the three strings. -
Process each permutation: For each ordering
(a, b, c)
:- First merge
a
andb
:temp = f(a, b)
- Then merge the result with
c
:s = f(temp, c)
- This gives us the merged string for this particular ordering
- First merge
-
Track the optimal result: Compare each merged string
s
with the current best answer:- If
ans
is empty (first iteration), setans = s
- If
len(s) < len(ans)
, updateans
tos
(shorter is better) - If
len(s) == len(ans) and s < ans
, updateans
tos
(same length but lexicographically smaller)
- If
-
Return the result: After checking all 6 permutations, return the optimal string stored in
ans
.
The time complexity is O(nΒ²)
for each merge operation (where n
is the length of the strings), and since we perform a constant number of merges (6 permutations Γ 2 merges each), the overall complexity remains manageable for typical string lengths.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a concrete example where a = "abc"
, b = "bca"
, and c = "aab"
.
Step 1: Understanding the merge function f(s, t)
First, let's see how the merge function works with f("abc", "bca")
:
- Check if "abc" contains "bca" β No
- Check if "bca" contains "abc" β No
- Look for overlap between end of "abc" and start of "bca":
- Check if last 3 chars of "abc" ("abc") match first 3 chars of "bca" ("bca") β No
- Check if last 2 chars of "abc" ("bc") match first 2 chars of "bca" ("bc") β Yes!
- Return "abc" + "bca"[2:] = "abc" + "a" = "abca"
Step 2: Try all 6 permutations
Let's trace through each permutation:
-
Order (a, b, c) = ("abc", "bca", "aab")
- Merge "abc" and "bca":
f("abc", "bca")
= "abca" (as shown above) - Merge "abca" and "aab":
f("abca", "aab")
- "abca" contains "aab"? No. "aab" contains "abca"? No
- Check overlaps: "a" at end of "abca" matches "a" at start of "aab"
- Result: "abca" + "ab" = "abcaab" (length 6)
- Merge "abc" and "bca":
-
Order (a, c, b) = ("abc", "aab", "bca")
- Merge "abc" and "aab":
f("abc", "aab")
= "aabc" (overlap "ab") - Merge "aabc" and "bca":
f("aabc", "bca")
= "aabca" (overlap "bc") - Result: "aabca" (length 5)
- Merge "abc" and "aab":
-
Order (b, a, c) = ("bca", "abc", "aab")
- Merge "bca" and "abc":
f("bca", "abc")
= "bcabc" (overlap "a") - Merge "bcabc" and "aab":
f("bcabc", "aab")
= "aabcabc" (no overlap) - Result: "aabcabc" (length 7)
- Merge "bca" and "abc":
-
Order (b, c, a) = ("bca", "aab", "abc")
- Merge "bca" and "aab":
f("bca", "aab")
= "bcaaab" (no overlap) - Merge "bcaaab" and "abc":
f("bcaaab", "abc")
= "bcaaabc" (overlap "ab") - Result: "bcaaabc" (length 7)
- Merge "bca" and "aab":
-
Order (c, a, b) = ("aab", "abc", "bca")
- Merge "aab" and "abc":
f("aab", "abc")
= "aabc" (overlap "ab") - Merge "aabc" and "bca":
f("aabc", "bca")
= "aabca" (overlap "bc") - Result: "aabca" (length 5)
- Merge "aab" and "abc":
-
Order (c, b, a) = ("aab", "bca", "abc")
- Merge "aab" and "bca":
f("aab", "bca")
= "aabca" (overlap "b") - Merge "aabca" and "abc":
f("aabca", "abc")
- "aabca" already contains "abc" (at position 1)
- Result: "aabca" (length 5)
- Merge "aab" and "bca":
Step 3: Select the optimal result
From all permutations:
- Length 5: "aabca" (appears 3 times)
- Length 6: "abcaab"
- Length 7: "aabcabc", "bcaaabc"
The shortest length is 5, and all three occurrences give us "aabca". We can verify this contains all three strings:
- "abc" appears at index 1: "aabca"
- "bca" appears at index 2: "aabca"
- "aab" appears at index 0: "aabca"
Therefore, the answer is "aabca".
Solution Implementation
1from itertools import permutations
2from typing import List
3
4class Solution:
5 def minimumString(self, a: str, b: str, c: str) -> str:
6 """
7 Find the minimum string that contains all three given strings as substrings.
8 If multiple solutions exist, return the lexicographically smallest one.
9
10 Args:
11 a: First string
12 b: Second string
13 c: Third string
14
15 Returns:
16 The minimum string containing all three strings as substrings
17 """
18
19 def merge_two_strings(first: str, second: str) -> str:
20 """
21 Merge two strings to create the shortest string containing both as substrings.
22
23 Args:
24 first: The first string to merge
25 second: The second string to merge
26
27 Returns:
28 The shortest merged string containing both input strings
29 """
30 # If first string is already contained in second, return second
31 if first in second:
32 return second
33
34 # If second string is already contained in first, return first
35 if second in first:
36 return first
37
38 first_len, second_len = len(first), len(second)
39
40 # Try to find the maximum overlap where end of first matches start of second
41 for overlap_length in range(min(first_len, second_len), 0, -1):
42 if first[-overlap_length:] == second[:overlap_length]:
43 # Found overlap, concatenate first with non-overlapping part of second
44 return first + second[overlap_length:]
45
46 # No overlap found, concatenate both strings completely
47 return first + second
48
49 result = ""
50
51 # Try all permutations of the three strings to find the optimal order
52 for string1, string2, string3 in permutations((a, b, c)):
53 # First merge string1 and string2, then merge the result with string3
54 merged_string = merge_two_strings(merge_two_strings(string1, string2), string3)
55
56 # Update result if this is the first permutation or if we found a better solution
57 # Better means: shorter length, or same length but lexicographically smaller
58 if (result == "" or
59 len(merged_string) < len(result) or
60 (len(merged_string) == len(result) and merged_string < result)):
61 result = merged_string
62
63 return result
64
1class Solution {
2 /**
3 * Find the minimum string that contains all three input strings as substrings.
4 * If multiple solutions exist, return the lexicographically smallest one.
5 *
6 * @param a First input string
7 * @param b Second input string
8 * @param c Third input string
9 * @return The minimum string containing all three strings
10 */
11 public String minimumString(String a, String b, String c) {
12 // Store the three strings in an array for easier access
13 String[] strings = {a, b, c};
14
15 // All possible permutations of indices [0, 1, 2]
16 int[][] permutations = {
17 {0, 1, 2}, {0, 2, 1}, {1, 0, 2},
18 {1, 2, 0}, {2, 1, 0}, {2, 0, 1}
19 };
20
21 String result = "";
22
23 // Try all permutations to find the optimal order of merging
24 for (int[] permutation : permutations) {
25 int firstIndex = permutation[0];
26 int secondIndex = permutation[1];
27 int thirdIndex = permutation[2];
28
29 // Merge first two strings, then merge the result with the third
30 String mergedFirstTwo = f(strings[firstIndex], strings[secondIndex]);
31 String mergedAll = f(mergedFirstTwo, strings[thirdIndex]);
32
33 // Update result if current merged string is better
34 // Better means: shorter length, or same length but lexicographically smaller
35 if ("".equals(result) ||
36 mergedAll.length() < result.length() ||
37 (mergedAll.length() == result.length() && mergedAll.compareTo(result) < 0)) {
38 result = mergedAll;
39 }
40 }
41
42 return result;
43 }
44
45 /**
46 * Merge two strings to create the shortest string that contains both as substrings.
47 *
48 * @param s First string
49 * @param t Second string
50 * @return The shortest merged string containing both s and t
51 */
52 private String f(String s, String t) {
53 // If s already contains t, return s
54 if (s.contains(t)) {
55 return s;
56 }
57
58 // If t already contains s, return t
59 if (t.contains(s)) {
60 return t;
61 }
62
63 int sLength = s.length();
64 int tLength = t.length();
65
66 // Try to find the maximum overlap between end of s and beginning of t
67 for (int overlapLength = Math.min(sLength, tLength); overlapLength > 0; overlapLength--) {
68 // Check if the last 'overlapLength' characters of s match
69 // the first 'overlapLength' characters of t
70 if (s.substring(sLength - overlapLength).equals(t.substring(0, overlapLength))) {
71 // Merge with overlap
72 return s + t.substring(overlapLength);
73 }
74 }
75
76 // No overlap found, concatenate the strings
77 return s + t;
78 }
79}
80
1class Solution {
2public:
3 string minimumString(string a, string b, string c) {
4 // Store the three input strings in a vector
5 vector<string> strings = {a, b, c};
6
7 // All possible permutations of indices [0, 1, 2]
8 vector<vector<int>> permutations = {
9 {0, 1, 2}, {0, 2, 1}, {1, 0, 2},
10 {1, 2, 0}, {2, 1, 0}, {2, 0, 1}
11 };
12
13 string result = "";
14
15 // Try all permutations to find the minimum string
16 for (auto& perm : permutations) {
17 int firstIdx = perm[0];
18 int secondIdx = perm[1];
19 int thirdIdx = perm[2];
20
21 // Merge strings in the current permutation order
22 // First merge strings[firstIdx] and strings[secondIdx], then merge with strings[thirdIdx]
23 string merged = mergeStrings(
24 mergeStrings(strings[firstIdx], strings[secondIdx]),
25 strings[thirdIdx]
26 );
27
28 // Update result if current merged string is better
29 // Better means: shorter length, or same length but lexicographically smaller
30 if (result.empty() ||
31 merged.size() < result.size() ||
32 (merged.size() == result.size() && merged < result)) {
33 result = merged;
34 }
35 }
36
37 return result;
38 }
39
40private:
41 // Merge two strings to create the shortest string containing both as substrings
42 string mergeStrings(string str1, string str2) {
43 // If str1 already contains str2, return str1
44 if (str1.find(str2) != string::npos) {
45 return str1;
46 }
47
48 // If str2 already contains str1, return str2
49 if (str2.find(str1) != string::npos) {
50 return str2;
51 }
52
53 int len1 = str1.size();
54 int len2 = str2.size();
55
56 // Find the maximum overlap between suffix of str1 and prefix of str2
57 for (int overlapLen = min(len1, len2); overlapLen > 0; --overlapLen) {
58 // Check if the last 'overlapLen' characters of str1 match
59 // the first 'overlapLen' characters of str2
60 if (str1.substr(len1 - overlapLen) == str2.substr(0, overlapLen)) {
61 // Merge by appending the non-overlapping part of str2 to str1
62 return str1 + str2.substr(overlapLen);
63 }
64 }
65
66 // No overlap found, concatenate the strings
67 return str1 + str2;
68 }
69};
70
1/**
2 * Finds the minimum string that contains all three input strings as substrings
3 * @param a - First input string
4 * @param b - Second input string
5 * @param c - Third input string
6 * @returns The lexicographically smallest string that contains all three strings
7 */
8function minimumString(a: string, b: string, c: string): string {
9 /**
10 * Merges two strings to create the shortest string containing both
11 * @param source - First string to merge
12 * @param target - Second string to merge
13 * @returns Merged string with minimum length
14 */
15 const mergeStrings = (source: string, target: string): string => {
16 // If source already contains target, return source
17 if (source.includes(target)) {
18 return source;
19 }
20
21 // If target already contains source, return target
22 if (target.includes(source)) {
23 return target;
24 }
25
26 const sourceLength = source.length;
27 const targetLength = target.length;
28
29 // Try to find overlapping suffix of source with prefix of target
30 for (let overlapLength = Math.min(sourceLength, targetLength); overlapLength > 0; --overlapLength) {
31 // Check if the last 'overlapLength' chars of source match first 'overlapLength' chars of target
32 if (source.slice(-overlapLength) === target.slice(0, overlapLength)) {
33 // Merge by appending non-overlapping part of target to source
34 return source + target.slice(overlapLength);
35 }
36 }
37
38 // No overlap found, concatenate both strings
39 return source + target;
40 };
41
42 // Array containing all three input strings
43 const strings: string[] = [a, b, c];
44
45 // All possible permutations of indices [0, 1, 2]
46 const permutations: number[][] = [
47 [0, 1, 2],
48 [0, 2, 1],
49 [1, 0, 2],
50 [1, 2, 0],
51 [2, 0, 1],
52 [2, 1, 0],
53 ];
54
55 // Initialize result string
56 let result = '';
57
58 // Try all permutations to find the optimal merging order
59 for (const [firstIndex, secondIndex, thirdIndex] of permutations) {
60 // Merge strings in the current permutation order
61 const mergedString = mergeStrings(
62 mergeStrings(strings[firstIndex], strings[secondIndex]),
63 strings[thirdIndex]
64 );
65
66 // Update result if current merged string is better (shorter or lexicographically smaller)
67 if (result === '' ||
68 mergedString.length < result.length ||
69 (mergedString.length === result.length && mergedString < result)) {
70 result = mergedString;
71 }
72 }
73
74 return result;
75}
76
Time and Space Complexity
Time Complexity: O(n^2)
The algorithm iterates through all 6 permutations of the three strings (constant factor). For each permutation, it calls function f
twice. Within function f
:
- The substring checks
s in t
andt in s
takeO(n^2)
time in the worst case using standard string matching - The loop runs at most
min(m, n)
iterations, which isO(n)
, and each iteration performs string slicing and comparison operations that takeO(n)
time - String concatenation operations take
O(n)
time
Therefore, the dominant operation is the substring checking with O(n^2)
complexity, making the overall time complexity O(n^2)
, where n
is the maximum length of the three strings.
Space Complexity: O(n)
The space usage includes:
- The
ans
variable storing a string of length at most3n
in the worst case:O(n)
- Temporary string
s
created in each iteration:O(n)
- String slicing operations create new strings:
O(n)
- The permutations generator uses constant extra space as it generates permutations one at a time
The space complexity is O(n)
, where n
is the maximum length of the three strings.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Overlap Detection Logic
A common mistake is incorrectly implementing the overlap detection between two strings. Developers often make errors in:
- Using wrong slice indices (e.g.,
first[:overlap_length]
instead offirst[-overlap_length:]
) - Not properly handling the concatenation after finding an overlap
- Checking overlaps in the wrong direction
Example of incorrect implementation:
# WRONG: Checking wrong parts of strings if first[:overlap_length] == second[:overlap_length]: # Both from start! return first + second[overlap_length:] # CORRECT: End of first should match beginning of second if first[-overlap_length:] == second[:overlap_length]: return first + second[overlap_length:]
2. Not Considering All Permutations
Some developers might try to optimize prematurely by only checking certain orders or assuming a greedy approach will work. This fails because the optimal order depends on the specific overlaps between strings.
Example scenario where order matters:
a = "abc"
,b = "bcd"
,c = "cde"
- Order (a,b,c) gives: "abcde" (length 5)
- Order (c,b,a) gives: "cdeabcd" (length 7)
Solution: Always check all 6 permutations as the code does.
3. Forgetting the Containment Check
A critical optimization that's easy to miss is checking whether one string already contains another. Without this check, the algorithm might produce unnecessarily long results.
Example of the issue:
# WITHOUT containment check
def merge_two_strings(first, second):
# Only checking overlaps
for overlap_length in range(min(len(first), len(second)), 0, -1):
if first[-overlap_length:] == second[:overlap_length]:
return first + second[overlap_length:]
return first + second
# If first="abcdef" and second="cde", this returns "abcdefcde" (length 9)
# But "cde" is already in "abcdef", so we should just return "abcdef" (length 6)
4. Incorrect Lexicographical Comparison
When comparing strings of equal length, developers might forget to check for lexicographical ordering or implement it incorrectly.
Incorrect approach:
# WRONG: Only considering length
if len(merged_string) < len(result):
result = merged_string
# CORRECT: Also consider lexicographical order for same length
if (result == "" or
len(merged_string) < len(result) or
(len(merged_string) == len(result) and merged_string < result)):
result = merged_string
5. Off-by-One Errors in Range
Using incorrect range bounds when checking overlaps is a frequent error:
Wrong:
# Missing potential overlaps by starting from wrong index
for overlap_length in range(min(first_len, second_len) - 1, 0, -1): # Off by one!
Correct:
# Check all possible overlaps from maximum to minimum
for overlap_length in range(min(first_len, second_len), 0, -1):
Prevention Tips:
- Test with edge cases where strings are substrings of each other
- Verify overlap logic with simple examples manually
- Use debug prints to trace the merging process
- Test with strings that have different optimal orderings
- Include test cases where lexicographical ordering matters
A heap is a ...?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!