Taro Logo

Scramble String

Hard
Amazon logo
Amazon
1 view
Topics:
StringsRecursionDynamic Programming

Let's explore the 'Scrambled String' problem.

Problem Statement:

We can scramble a string s to get a string t using the following algorithm:

  1. If the length of the string is 1, stop.
  2. If the length of the string is > 1, do the following:
    • Split the string into two non-empty substrings at a random index, i.e., if the string is s, divide it to x and y where s = x + y.
    • Randomly decide to swap the two substrings or to keep them in the same order. i.e., after this step, s may become s = x + y or s = y + x.
    • Apply step 1 recursively on each of the two substrings x and y.

Given two strings s1 and s2 of the same length, return true if s2 is a scrambled string of s1, otherwise, return false.

Clarification:

Essentially, a scrambled string is derived from the original string by recursively partitioning it into two non-empty parts and optionally swapping those parts.

Examples:

  1. s1 = "great", s2 = "rgeat"

    • Output: true
    • Explanation: "great" can be scrambled to "rgeat" through a series of splits and swaps as shown in the original prompt's example.
  2. s1 = "abcde", s2 = "caebd"

    • Output: false
  3. s1 = "a", s2 = "a"

    • Output: true

Constraints:

  • s1.length == s2.length
  • 1 <= s1.length <= 30
  • s1 and s2 consist of lowercase English letters.

Can you describe an algorithm to determine if s2 is a scrambled string of s1?

Solution


Scrambled String Problem

Problem Description

Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1. A scrambled string is derived from s1 by recursively swapping non-empty substrings.

Naive Approach (Brute Force)

The most straightforward approach is to recursively explore all possible ways to split and swap s1 to see if any of them result in s2. This involves generating all possible scramblings of s1 and comparing them with s2. However, this approach is highly inefficient due to the exponential number of possible scramblings.

Code (Illustrative, not practical for larger strings)

def is_scramble_brute_force(s1, s2):
    if len(s1) != len(s2):
        return False
    if s1 == s2:
        return True
    if sorted(s1) != sorted(s2):
        return False

    n = len(s1)
    for i in range(1, n):
        # No swap
        if is_scramble_brute_force(s1[:i], s2[:i]) and is_scramble_brute_force(s1[i:], s2[i:]):
            return True
        # Swap
        if is_scramble_brute_force(s1[:i], s2[n-i:]) and is_scramble_brute_force(s1[i:], s2[:n-i]):
            return True
    return False

Time Complexity

Exponential, O(n!) in the worst case due to exploring all possible splits and swaps.

Space Complexity

O(n) due to the recursion depth.

Optimal Approach (Dynamic Programming)

We can optimize this process using dynamic programming by storing the results of subproblems to avoid redundant calculations. We'll use a 3D boolean array dp[i][j][k] where:

  • i is the starting index of the substring in s1.
  • j is the starting index of the substring in s2.
  • k is the length of the substring.

dp[i][j][k] will be true if the substring of s1 starting at i with length k is a scrambled string of the substring of s2 starting at j with length k, and false otherwise.

Algorithm

  1. Base Case: If k == 1, then dp[i][j][1] = (s1[i] == s2[j]).
  2. Iteration: For lengths k from 2 to n, iterate through all possible starting indices i and j:
    • For each possible split point l from 1 to k-1:
      • Check if s1[i:i+l] is a scrambled string of s2[j:j+l] AND s1[i+l:i+k] is a scrambled string of s2[j+l:j+k].
      • OR
      • Check if s1[i:i+l] is a scrambled string of s2[j+k-l:j+k] AND s1[i+l:i+k] is a scrambled string of s2[j:j+k-l].
      • If either of these conditions is true, set dp[i][j][k] = true.
  3. The final result is stored in dp[0][0][n].

Code (Dynamic Programming)

def is_scramble_dp(s1, s2):
    n = len(s1)
    if n != len(s2):
        return False
    if s1 == s2:
        return True
    if sorted(s1) != sorted(s2):
        return False

    dp = [[[False] * (n + 1) for _ in range(n)] for _ in range(n)]

    # Base case: length 1 substrings
    for i in range(n):
        for j in range(n):
            dp[i][j][1] = (s1[i] == s2[j])

    # Build up the dp table
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            for j in range(n - length + 1):
                for k in range(1, length):
                    # No swap
                    if dp[i][j][k] and dp[i+k][j+k][length-k]:
                        dp[i][j][length] = True
                        break
                    # Swap
                    if dp[i][j+length-k][k] and dp[i+k][j][length-k]:
                        dp[i][j][length] = True
                        break

    return dp[0][0][n]

Time Complexity

O(n^4), where n is the length of the strings. This is due to the four nested loops (three for iterating through the dp table and one for the split point).

Space Complexity

O(n^3) to store the dp table.

Edge Cases

  1. Empty Strings: If both strings are empty, they are considered scrambled strings of each other (return true).
  2. Unequal Lengths: If the lengths of the strings are different, they cannot be scrambled strings (return false).
  3. Strings of Length 1: If both strings have length 1, they are scrambled strings if and only if they are equal.
  4. Strings with different character counts: If the character counts are different, they cannot be scrambled strings (return false). We can use the sorted character representation as a fast check.