Facebook Pixel

555. Split Concatenated Strings πŸ”’

Problem Description

You are given an array of strings strs. Your task is to create a loop by concatenating these strings together, where you have the option to reverse any string before concatenation. After forming the loop, you need to find a cutting point that produces the lexicographically largest possible string.

The process involves two main steps:

  1. Form a loop: Concatenate all strings from strs in their given order to form a circular arrangement. Before concatenating, you can choose to reverse any string or keep it as is. For example, if strs = ["abc", "xyz"], you could form loops like "abcxyz", "cbaxyz", "abczyx", or "cbazyx".

  2. Cut the loop: Choose any position in the loop as a breakpoint. This converts the circular string into a regular linear string starting from the cut position. For instance, if you have a loop "abcxyz" and cut between 'c' and 'x', you get "xyzabc".

Your goal is to find the combination of string reversals and cutting position that produces the lexicographically largest possible result among all valid configurations.

For example, with strs = ["abc", "xyz"]:

  • You could reverse "abc" to get "cba" and keep "xyz" to form loop "cbaxyz"
  • Cutting at different positions gives different results: "cbaxyz", "baxyzc", "axyzcp", "xyzcba", "yzcbax", "zcbaxy"
  • Among all possible combinations of reversals and cuts, you need to return the largest string

The output should be the lexicographically largest string achievable through this process.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To find the lexicographically largest string, we need to make locally optimal choices at each step. Let's think about this problem systematically.

First, consider the strings we're working with. For each individual string in strs, we have the choice to reverse it or not. Since we want the largest possible result, we should choose the version (original or reversed) that is lexicographically larger for most strings. This gives us a better starting point.

However, there's a catch. The string where we make the cut is special because it will be split into two parts. For the cut string, we might want to use the "worse" version if it allows us to position a better character at the beginning of our final result.

Let's break down the strategy:

  1. Preprocessing: For all strings except the one we're cutting, we want the lexicographically larger version between the original and reversed. This maximizes the middle portion of our result.

  2. Finding the cut position: We need to try every possible string as the cut position and every possible character within that string as the exact cut point. Why? Because the character immediately after the cut becomes the first character of our result, which has the highest impact on lexicographical ordering.

  3. Handling the cut string: When we cut a string at position j, we get:

    • Suffix part a = s[j:] (this becomes the beginning of our result)
    • Prefix part b = s[:j] (this goes to the end of our result)

    We need to consider both orientations of the cut string:

    • Using the string as-is: result = a + middle + b
    • Using the reversed string: result = b[::-1] + middle + a[::-1]

The key insight is that we want to maximize the beginning of our final string. By trying all possible cut positions and both orientations of the cut string, we ensure we find the configuration that places the lexicographically largest possible substring at the beginning of our result.

This greedy approach works because lexicographical comparison prioritizes earlier characters - finding the best possible beginning is more important than optimizing later parts of the string.

Learn more about Greedy patterns.

Solution Approach

Let's walk through the implementation step by step:

Step 1: Optimize each string individually

strs = [s[::-1] if s[::-1] > s else s for s in strs]

We first preprocess the array by replacing each string with its lexicographically larger version (original or reversed). This ensures that for most positions, we're working with the best possible strings. The comparison s[::-1] > s automatically compares strings lexicographically in Python.

Step 2: Initialize the answer

ans = ''.join(strs)

We start with a baseline answer by simply joining all the optimized strings. This represents one possible configuration without considering different cut positions.

Step 3: Try each string as the cut position

for i, s in enumerate(strs):
    t = ''.join(strs[i + 1 :]) + ''.join(strs[:i])

For each string at position i, we treat it as the string that will be cut. We construct t, which represents all the other strings in order:

  • strs[i + 1:] - all strings after position i
  • strs[:i] - all strings before position i

This t forms the middle portion of our result (the part that won't be split).

Step 4: Try each character position within the cut string

for j in range(len(s)):
    a = s[j:]  # suffix starting from position j
    b = s[:j]  # prefix up to position j

Within the selected string s, we try cutting at each position j. This splits s into:

  • Suffix a: becomes the beginning of our result
  • Prefix b: goes to the end of our result

Step 5: Consider both orientations

ans = max(ans, a + t + b)
ans = max(ans, b[::-1] + t + a[::-1])

For each cut position, we consider two cases:

  1. Using the current orientation: a + t + b
    • This uses the string s as it currently is (already optimized in Step 1)
  2. Using the opposite orientation: b[::-1] + t + a[::-1]
    • This effectively uses the reverse of string s
    • When we reverse s, the prefix b becomes a[::-1] and the suffix a becomes b[::-1]

We update ans with the maximum of these possibilities using Python's built-in max() function, which compares strings lexicographically.

Time Complexity: O(n * m^2) where n is the number of strings and m is the average length of strings. We iterate through each string (O(n)), and for each string, we try each cutting position (O(m)) and perform string concatenations (O(m)).

Space Complexity: O(n * m) for storing the concatenated strings and temporary results.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with strs = ["abc", "xyz"].

Step 1: Optimize each string

  • For "abc": Compare "abc" vs "cba" β†’ "cba" > "abc", so use "cba"
  • For "xyz": Compare "xyz" vs "zyx" β†’ "zyx" > "xyz", so use "zyx"
  • Now strs = ["cba", "zyx"]

Step 2: Initialize baseline answer

  • ans = "cbazyx"

Step 3-5: Try all cut positions

Cut in first string "cba":

  • i = 0, s = "cba", t = "zyx" (remaining strings)
    • Cut at j=0: a = "cba", b = ""
      • Current orientation: "cba" + "zyx" + "" = "cbazyx"
      • Opposite orientation: "" + "zyx" + "abc" = "zyxabc"
      • Update: ans = max("cbazyx", "cbazyx", "zyxabc") = "zyxabc"
    • Cut at j=1: a = "ba", b = "c"
      • Current orientation: "ba" + "zyx" + "c" = "bazyxc"
      • Opposite orientation: "c" + "zyx" + "ab" = "czyxab"
      • Keep: ans = "zyxabc" (still largest)
    • Cut at j=2: a = "a", b = "cb"
      • Current orientation: "a" + "zyx" + "cb" = "azyxcb"
      • Opposite orientation: "bc" + "zyx" + "a" = "bczyxa"
      • Keep: ans = "zyxabc"

Cut in second string "zyx":

  • i = 1, s = "zyx", t = "cba" (remaining strings)
    • Cut at j=0: a = "zyx", b = ""
      • Current orientation: "zyx" + "cba" + "" = "zyxcba"
      • Opposite orientation: "" + "cba" + "xyz" = "cbaxyz"
      • Update: ans = max("zyxabc", "zyxcba", "cbaxyz") = "zyxcba"
    • Cut at j=1: a = "yx", b = "z"
      • Current orientation: "yx" + "cba" + "z" = "yxcbaz"
      • Opposite orientation: "z" + "cba" + "xy" = "zcbaxy"
      • Keep: ans = "zyxcba"
    • Cut at j=2: a = "x", b = "zy"
      • Current orientation: "x" + "cba" + "zy" = "xcbazy"
      • Opposite orientation: "yz" + "cba" + "x" = "yzcbax"
      • Keep: ans = "zyxcba"

Final Result: "zyxcba"

This example shows how the algorithm systematically explores all possible configurations. The winning configuration came from cutting the second string at position 0 with its current (optimized) orientation, which placed 'z' (the largest possible character) at the beginning of the result.

Solution Implementation

1class Solution:
2    def splitLoopedString(self, strs: List[str]) -> str:
3        # Step 1: For each string, choose the lexicographically larger between itself and its reverse
4        # This ensures we start with the best possible version of each string
5        optimized_strs = []
6        for string in strs:
7            reversed_string = string[::-1]
8            if reversed_string > string:
9                optimized_strs.append(reversed_string)
10            else:
11                optimized_strs.append(string)
12      
13        # Initialize the answer with the concatenation of all optimized strings
14        max_result = ''.join(optimized_strs)
15      
16        # Step 2: Try different cut points by considering each string position
17        for index, current_string in enumerate(optimized_strs):
18            # Build the middle part: strings after current + strings before current
19            # This represents the fixed part when we cut at position 'index'
20            after_current = ''.join(optimized_strs[index + 1:])
21            before_current = ''.join(optimized_strs[:index])
22            middle_part = after_current + before_current
23          
24            # Step 3: For the current string, try all possible cut positions
25            for cut_position in range(len(current_string)):
26                # Split current string at position 'cut_position'
27                prefix = current_string[cut_position:]  # Part that goes to the beginning
28                suffix = current_string[:cut_position]  # Part that goes to the end
29              
30                # Try configuration 1: Use the optimized version of current string
31                # Format: prefix + middle_part + suffix
32                candidate1 = prefix + middle_part + suffix
33                max_result = max(max_result, candidate1)
34              
35                # Try configuration 2: Use the reverse of current string
36                # When reversed: suffix becomes prefix (reversed) and prefix becomes suffix (reversed)
37                candidate2 = suffix[::-1] + middle_part + prefix[::-1]
38                max_result = max(max_result, candidate2)
39      
40        return max_result
41
1class Solution {
2    public String splitLoopedString(String[] strs) {
3        int arrayLength = strs.length;
4      
5        // Step 1: For each string, keep the lexicographically larger between itself and its reverse
6        for (int i = 0; i < arrayLength; ++i) {
7            String original = strs[i];
8            String reversed = new StringBuilder(original).reverse().toString();
9          
10            // If reversed string is lexicographically larger, replace the original
11            if (original.compareTo(reversed) < 0) {
12                strs[i] = reversed;
13            }
14        }
15      
16        String maxResult = "";
17      
18        // Step 2: Try each string as the "cut point" string
19        for (int cutIndex = 0; cutIndex < arrayLength; ++cutIndex) {
20            String currentString = strs[cutIndex];
21          
22            // Build the middle part: concatenate all other strings in order
23            // Starting from the string after cutIndex, wrapping around to before cutIndex
24            StringBuilder middlePart = new StringBuilder();
25          
26            // Append strings from cutIndex+1 to end
27            for (int j = cutIndex + 1; j < arrayLength; ++j) {
28                middlePart.append(strs[j]);
29            }
30          
31            // Append strings from start to cutIndex-1
32            for (int j = 0; j < cutIndex; ++j) {
33                middlePart.append(strs[j]);
34            }
35          
36            String middle = middlePart.toString();
37          
38            // Step 3: Try all possible cut positions within the current string
39            for (int splitPos = 0; splitPos < currentString.length(); ++splitPos) {
40                // Split current string at position splitPos
41                String prefix = currentString.substring(splitPos);      // Part after split
42                String suffix = currentString.substring(0, splitPos);    // Part before split
43              
44                // Option 1: Use the string as-is (prefix + middle + suffix)
45                String candidate1 = prefix + middle + suffix;
46                if (maxResult.compareTo(candidate1) < 0) {
47                    maxResult = candidate1;
48                }
49              
50                // Option 2: Use the reversed string
51                // When reversed, suffix becomes prefix (reversed) and prefix becomes suffix (reversed)
52                String candidate2 = new StringBuilder(suffix)
53                          .reverse()
54                          .append(middle)
55                          .append(new StringBuilder(prefix).reverse().toString())
56                          .toString();
57                        
58                if (maxResult.compareTo(candidate2) < 0) {
59                    maxResult = candidate2;
60                }
61            }
62        }
63      
64        return maxResult;
65    }
66}
67
1class Solution {
2public:
3    string splitLoopedString(vector<string>& strs) {
4        // Step 1: For each string, keep the lexicographically larger one between itself and its reverse
5        for (auto& str : strs) {
6            string reversed{str.rbegin(), str.rend()};
7            str = max(str, reversed);
8        }
9      
10        int n = strs.size();
11        string result = "";
12      
13        // Step 2: Try cutting at each string position
14        for (int i = 0; i < n; ++i) {
15            auto& currentStr = strs[i];
16          
17            // Build the middle part: concatenate all other strings in order (i+1 to n-1, then 0 to i-1)
18            string middlePart;
19            for (int j = i + 1; j < n; ++j) {
20                middlePart += strs[j];
21            }
22            for (int j = 0; j < i; ++j) {
23                middlePart += strs[j];
24            }
25          
26            // Step 3: Try all possible cut positions within the current string
27            for (int cutPos = 0; cutPos < currentStr.size(); ++cutPos) {
28                // Case 1: Use the current string as is, cut at position cutPos
29                string prefix = currentStr.substr(cutPos);        // Part after cut point
30                string suffix = currentStr.substr(0, cutPos);     // Part before cut point
31                string candidate = prefix + middlePart + suffix;
32              
33                // Update result if we found a lexicographically larger string
34                if (result < candidate) {
35                    result = candidate;
36                }
37              
38                // Case 2: Use the reverse of current string, cut at the same position
39                reverse(prefix.begin(), prefix.end());
40                reverse(suffix.begin(), suffix.end());
41                candidate = suffix + middlePart + prefix;
42              
43                // Update result if we found a lexicographically larger string
44                if (result < candidate) {
45                    result = candidate;
46                }
47            }
48        }
49      
50        return result;
51    }
52};
53
1function splitLoopedString(strs: string[]): string {
2    // Step 1: For each string, keep the lexicographically larger one between itself and its reverse
3    for (let i = 0; i < strs.length; i++) {
4        const reversed = strs[i].split('').reverse().join('');
5        strs[i] = strs[i] > reversed ? strs[i] : reversed;
6    }
7  
8    const n = strs.length;
9    let result = "";
10  
11    // Step 2: Try cutting at each string position
12    for (let i = 0; i < n; i++) {
13        const currentStr = strs[i];
14      
15        // Build the middle part: concatenate all other strings in order (i+1 to n-1, then 0 to i-1)
16        let middlePart = "";
17        for (let j = i + 1; j < n; j++) {
18            middlePart += strs[j];
19        }
20        for (let j = 0; j < i; j++) {
21            middlePart += strs[j];
22        }
23      
24        // Step 3: Try all possible cut positions within the current string
25        for (let cutPos = 0; cutPos < currentStr.length; cutPos++) {
26            // Case 1: Use the current string as is, cut at position cutPos
27            let prefix = currentStr.substring(cutPos);        // Part after cut point
28            let suffix = currentStr.substring(0, cutPos);     // Part before cut point
29            let candidate = prefix + middlePart + suffix;
30          
31            // Update result if we found a lexicographically larger string
32            if (result < candidate) {
33                result = candidate;
34            }
35          
36            // Case 2: Use the reverse of current string, cut at the same position
37            const reversedPrefix = prefix.split('').reverse().join('');
38            const reversedSuffix = suffix.split('').reverse().join('');
39            candidate = reversedSuffix + middlePart + reversedPrefix;
40          
41            // Update result if we found a lexicographically larger string
42            if (result < candidate) {
43                result = candidate;
44            }
45        }
46    }
47  
48    return result;
49}
50

Time and Space Complexity

The time complexity is O(nΒ²), where n is the total length of all strings combined in the input array strs.

Breaking down the analysis:

  • The list comprehension [s[::-1] if s[::-1] > s else s for s in strs] takes O(n) time, where each string reversal and comparison is O(length of string).
  • The outer loop iterates through each string in strs, which is O(m) iterations where m is the number of strings.
  • For each string at position i, we concatenate strings using ''.join(strs[i + 1 :]) + ''.join(strs[:i]), which takes O(n) time.
  • The inner loop iterates through each character position j in the current string, up to the length of that string.
  • Inside the inner loop, we perform string slicing operations (s[j:], s[:j]) and reversals (b[::-1], a[::-1]), each taking O(length of current string) time.
  • The max comparisons with string concatenations take O(n) time each.

Since we iterate through every character position across all strings (total n positions) and perform O(n) operations for each position, the overall time complexity is O(nΒ²).

The space complexity is O(n):

  • The modified strs array stores strings with total length n.
  • The variable t stores a concatenated string of length up to n.
  • The variables a, b, and ans store strings of length up to n.
  • All intermediate string operations create temporary strings but don't exceed O(n) space at any given time.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Not Considering Both Orientations for the Cut String

The Problem: A common mistake is to only use the pre-optimized version of each string when trying different cut positions. Since we initially optimize each string to its lexicographically larger version, developers might assume this optimization is sufficient for all cases.

# INCORRECT approach - only using the optimized version
for index, current_string in enumerate(optimized_strs):
    middle_part = ''.join(optimized_strs[index + 1:]) + ''.join(optimized_strs[:index])
    for cut_position in range(len(current_string)):
        prefix = current_string[cut_position:]
        suffix = current_string[:cut_position]
        # Only considering one orientation
        candidate = prefix + middle_part + suffix
        max_result = max(max_result, candidate)

Why It's Wrong: The initial optimization chooses the best version when the string is kept whole. However, when we cut a string at different positions, the opposite orientation might produce a better result for specific cut points.

Example: Consider strs = ["lc", "love"]:

  • Initial optimization: "lc" stays as "lc" (since "cl" < "lc")
  • If we cut "lc" at position 1: we get "c" + rest + "l"
  • But if we use "cl" (reverse) and cut at position 1: we get "l" + rest + "c"
  • The second option starting with "l" might be lexicographically larger than starting with "c"

Solution: Always test both the optimized orientation and its reverse for the string being cut:

for cut_position in range(len(current_string)):
    prefix = current_string[cut_position:]
    suffix = current_string[:cut_position]
  
    # Test with current orientation
    candidate1 = prefix + middle_part + suffix
    max_result = max(max_result, candidate1)
  
    # Test with reversed orientation
    candidate2 = suffix[::-1] + middle_part + prefix[::-1]
    max_result = max(max_result, candidate2)

Pitfall 2: Incorrect String Reconstruction After Cutting

The Problem: When cutting a loop at a specific position within a string, developers might incorrectly reconstruct the final string by misunderstanding how the pieces should be arranged.

# INCORRECT - wrong order of concatenation
for cut_position in range(len(current_string)):
    prefix = current_string[:cut_position]  # Wrong slice
    suffix = current_string[cut_position:]  # Wrong slice
    # Incorrect assembly
    candidate = middle_part + prefix + suffix

Why It's Wrong: When we cut at position j in string s:

  • The part from position j to the end (s[j:]) becomes the beginning of our result
  • The part from start to position j (s[:j]) goes to the end of our result
  • The middle part (other strings) stays in between

Solution: Ensure correct slicing and ordering:

for cut_position in range(len(current_string)):
    prefix = current_string[cut_position:]  # This goes to the beginning
    suffix = current_string[:cut_position]  # This goes to the end
    candidate = prefix + middle_part + suffix  # Correct order

Pitfall 3: Not Initializing with a Valid Answer

The Problem: Starting with an empty string or not considering the case where no cut provides a better result:

# INCORRECT - starting with empty string
max_result = ""

Why It's Wrong: The concatenation of optimized strings without any special cutting is a valid answer. If no cut position improves upon this, it should be returned.

Solution: Initialize with the simple concatenation of optimized strings:

max_result = ''.join(optimized_strs)

This ensures we always have a valid baseline answer that represents cutting at position 0 of the first string.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More