Taro Logo

Equalize Strings by Adding or Removing Characters at Ends

Medium
Asked by:
Profile picture
22 views
Topics:
StringsDynamic Programming

You are given two strings s and t. You can perform the following operation any number of times:

  • Choose a non-empty prefix of string s or string t, delete it and add its reverse to the end of the same string.

Return the minimum number of operations to make the strings s and t equal, or -1 if it is impossible.

A prefix of a string is a substring starting from the beginning of the string.

Example 1:

Input: s = "abcde", t = "abcde"
Output: 0
Explanation: s and t are already equal.

Example 2:

Input: s = "leetcode", t = "lyleetcode"
Output: 1
Explanation: We can delete the prefix "l" from t and add its reverse "l" at the end of t so that t becomes "yleetcodel".
Since s = t, we return 1.

Example 3:

Input: s = "abcd", t = "abcde"
Output: -1
Explanation: It is impossible to make the strings equal.

Constraints:

  • 1 <= s.length, t.length <= 105
  • s and t consist of lowercase English letters.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. Can the input strings be empty or null?
  2. Are the input strings case-sensitive? Should I treat 'abc' and 'ABC' as different strings?
  3. What is the maximum length of the input strings?
  4. If after removing characters, the strings cannot be made equal, what should I return? Should I return -1, an error message, or is it guaranteed that the strings can always be made equal?
  5. Are there any specific character sets to expect, or should I assume it will be general ASCII/Unicode characters?

Brute Force Solution

Approach

The brute force method for making two strings equal involves exploring all possible ways to shorten each string by removing characters from their ends. We essentially try every combination of removing some characters from the start and end of each string until we find the longest matching part.

Here's how the algorithm would work step-by-step:

  1. Start by comparing the strings as they are, without removing anything.
  2. Next, consider removing one character from the beginning of the first string and see if the remaining parts of the strings match.
  3. Then, remove two characters from the beginning of the first string, and so on, until we've removed all possible starting sections of the first string.
  4. After trying all beginning removals from the first string, do the same thing by removing characters from the end of the first string, testing all possible lengths of the remaining part against the second string.
  5. Continue by trying all combinations of removing characters from both the beginning and the end of the first string.
  6. Now, repeat the entire process, but this time, focus on removing characters from the beginning and end of the second string instead of the first string, and compare what remains to the original first string.
  7. During all these removal and comparison steps, keep track of the longest matching part we find between the modified strings.
  8. The number of removals that created the longest matching part is the answer: the minimum number of characters we need to remove to make the strings share a common part.

Code Implementation

def equalize_strings_brute_force(first_string, second_string):
    minimum_removals = float('inf')

    for first_string_length in range(len(first_string) + 1):
        for second_string_length in range(len(second_string) + 1):
            # Check if the substrings are equal
            if first_string[:first_string_length] == second_string[:second_string_length]:
                removals = (len(first_string) - first_string_length) + \
                           (len(second_string) - second_string_length)

                # Update the minimum removals if a better solution is found
                minimum_removals = min(minimum_removals, removals)

    #Check all ways to shorten the first string from the end.
    for first_string_end_index in range(len(first_string) + 1):
        for second_string_length in range(len(second_string) + 1):
            if first_string[len(first_string)-first_string_end_index:] == second_string[:second_string_length]:
                removals = (len(first_string) - first_string_end_index) + first_string_end_index - (len(first_string) - first_string_end_index)  + (len(second_string) - second_string_length)
                minimum_removals = min(minimum_removals, removals)

    # Now try shortening both strings simultaneously from the end
    for first_string_end_index in range(len(first_string) + 1):
        for second_string_end_index in range(len(second_string) + 1):
            #Compare shortened string ends, both being chopped
            if first_string[len(first_string)-first_string_end_index:] == second_string[len(second_string)-second_string_end_index:]:
                removals = first_string_end_index - (len(first_string) - first_string_end_index) + (len(first_string)- first_string_end_index) + second_string_end_index - (len(second_string) - second_string_end_index) + (len(second_string) - second_string_end_index) # Calculate total removals
                minimum_removals = min(minimum_removals, removals) # Keep minimum

    if minimum_removals == float('inf'):
        return len(first_string) + len(second_string)

    return minimum_removals

Big(O) Analysis

Time Complexity
O(n^3) – Let n be the maximum length of the two input strings. The brute force approach involves iterating through all possible prefixes and suffixes of both strings. For each string, we consider up to n prefixes and n suffixes. For each combination of prefix and suffix removals (yielding a substring) from one string, we compare it against every possible substring from the other string which also takes O(n) in the worst case using brute force comparison. This results in O(n) * O(n) substrings to create from string one * O(n) time to compare with string two. Therefore, the time complexity is O(n^3).
Space Complexity
O(1) – The described brute force algorithm primarily involves string comparisons and iterative shortening of strings by removing characters from the ends. It does not explicitly use any auxiliary data structures like arrays, hash maps, or stacks to store intermediate results that scale with the input string lengths. Only a constant number of variables are used to track the longest matching part found so far and indices during comparisons. Therefore, the space complexity remains constant, regardless of the size of the input strings.

Optimal Solution

Approach

The most efficient way to solve this problem is to find the longest sequence of characters that the two strings share. We achieve this by focusing on identifying commonalities and minimizing the characters removed from either end.

Here's how the algorithm would work step-by-step:

  1. Imagine you have two ropes and you want to find the longest matching segment within them.
  2. Start by comparing the strings from their beginnings. Identify the longest identical beginning segment.
  3. Now, compare the strings from their ends. Find the longest identical ending segment.
  4. Consider that the longest matching sequence may not be at the ends; it could be a shared portion somewhere in the middle of both strings.
  5. To find this, try out different pairings of characters. For example, take a character from the first string and see if it exists in the second string. If it does, check how many characters follow it in each string are identical.
  6. Keep track of the longest common segment you have found so far.
  7. Once you have explored all possible common segments, add up the lengths of the longest identical beginning, the longest identical ending, and the longest segment found in the middle.
  8. The goal is to maximize the total length of shared segments across the strings.
  9. Subtract this maximal length from the combined length of both strings. The result is the minimum number of characters you must remove to make the strings equal in terms of the common sequence.

Code Implementation

def equalize_strings(first_string, second_string):
    longest_common_sequence_length = 0
    for i in range(len(first_string)):
        for j in range(len(second_string)):
            current_length = 0
            string_one_index = i
            string_two_index = j
            while (string_one_index < len(first_string) and
                   string_two_index < len(second_string) and
                   first_string[string_one_index] == second_string[string_two_index]):
                current_length += 1
                string_one_index += 1
                string_two_index += 1

            # Determine longest common substring length
            longest_common_sequence_length = max(longest_common_sequence_length, current_length)

    prefix_length_first_string = 0
    suffix_length_first_string = 0
    prefix_length_second_string = 0
    suffix_length_second_string = 0

    for i in range(len(first_string)):
        for j in range(len(second_string)):
            current_length = 0
            string_one_index = i
            string_two_index = j
            while (string_one_index < len(first_string) and
                   string_two_index < len(second_string) and
                   first_string[string_one_index] == second_string[string_two_index]):
                current_length += 1
                string_one_index += 1
                string_two_index += 1

            if current_length == longest_common_sequence_length:
                #Calculate lengths of non-matching prefixes.
                prefix_length_first_string = i
                suffix_length_first_string = len(first_string) - (i + current_length)
                prefix_length_second_string = j
                suffix_length_second_string = len(second_string) - (j + current_length)
                
                break
        else:
            continue
        break

    # Sum up chars outside of the longest subsequence
    total_removals = (
        prefix_length_first_string +
        suffix_length_first_string +
        prefix_length_second_string +
        suffix_length_second_string
    )

    return total_removals

Big(O) Analysis

Time Complexity
O(n^2) – The solution involves comparing the two strings, each of length n, from their beginnings and ends, which takes O(n) time. The core of the algorithm involves finding the longest common segment within the strings, which requires iterating through each character of the first string (n iterations) and for each of those characters, searching for matching characters in the second string (n iterations). For each potential match, the algorithm extends the match to find the length of the common segment. Thus, the worst case involves n * n comparisons. Hence the time complexity is O(n^2).
Space Complexity
O(N^2) – The algorithm searches for the longest common segment by trying out different pairings of characters. In the worst-case scenario, we might need to store intermediate results of the lengths of these common segments, potentially requiring a matrix or similar data structure to track the longest common subsequence. Let N be the length of the longer string. The storage of intermediate results to track the lengths of common substrings contributes O(N^2) space in the worst case. Therefore, the auxiliary space complexity is O(N^2).

Edge Cases

Both input strings are null or empty
How to Handle:
Return 0 if both strings are null or empty since they're already equal.
One string is null or empty, and the other is not
How to Handle:
Return the length of the non-null/non-empty string as that's the number of removals required.
Strings are identical
How to Handle:
Return 0 as no operations are needed.
Strings have no characters in common
How to Handle:
Return the sum of the lengths of the two strings, indicating all characters must be removed.
Very long strings (close to maximum allowed length)
How to Handle:
The algorithm should still produce the correct result within acceptable time complexity, especially dynamic programming approaches; consider memory usage for long strings.
One string is a prefix/suffix of the other
How to Handle:
This is a case where the LCS approach will readily identify the longest common string, requiring removals based on length differences.
Strings with repeated characters (e.g., 'aaaa' and 'bbbb')
How to Handle:
The LCS approach handles repeated characters correctly as it looks for the longest sequence, whether repeated or unique.
Integer overflow possible when calculating the length of the common substring or the total number of operations for extremely long strings
How to Handle:
Consider using a data type that can handle large values or ensure operations are performed using appropriate data types.