Let's explore the 'Scrambled String' problem.
Problem Statement:
We can scramble a string s
to get a string t
using the following algorithm:
s
, divide it to x
and y
where s = x + y
.s
may become s = x + y
or s = y + x
.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:
s1 = "great", s2 = "rgeat"
true
s1 = "abcde", s2 = "caebd"
false
s1 = "a", s2 = "a"
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
?
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.
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.
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
Exponential, O(n!) in the worst case due to exploring all possible splits and swaps.
O(n) due to the recursion depth.
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.
k == 1
, then dp[i][j][1] = (s1[i] == s2[j])
.k
from 2 to n
, iterate through all possible starting indices i
and j
:
l
from 1 to k-1
:
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]
.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]
.dp[i][j][k] = true
.dp[0][0][n]
.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]
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).
O(n^3) to store the dp
table.
true
).false
).false
). We can use the sorted character representation as a fast check.