We can scramble a string s to get a string t using the following algorithm:
- If the length of the string is 1, stop.
- 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 toxandywheres = x + y. - Randomly decide to swap the two substrings or to keep them in the same order. i.e., after this step,
smay becomes = x + yors = y + x. - Apply step 1 recursively on each of the two substrings
xandy.
- Split the string into two non-empty substrings at a random index, i.e., if the string is
Given two strings s1 and s2 of the same length, return true if s2 is a scrambled string of s1, otherwise, return false.
Example 1:
Input: s1 = "great", s2 = "rgeat" Output: true Explanation: One possible scenario applied on s1 is: "great" --> "gr/eat" // divide at random index. "gr/eat" --> "gr/eat" // random decision is not to swap the two substrings and keep them in order. "gr/eat" --> "g/r / e/at" // apply the same algorithm recursively on both substrings. divide at random index each of them. "g/r / e/at" --> "r/g / e/at" // random decision was to swap the first substring and to keep the second substring in the same order. "r/g / e/at" --> "r/g / e/ a/t" // again apply the algorithm recursively, divide "at" to "a/t". "r/g / e/ a/t" --> "r/g / e/ a/t" // random decision is to keep both substrings in the same order. The algorithm stops now, and the result string is "rgeat" which is s2. As one possible scenario led s1 to be scrambled to s2, we return true.
Example 2:
Input: s1 = "abcde", s2 = "caebd" Output: false
Example 3:
Input: s1 = "a", s2 = "a" Output: true
Constraints:
s1.length == s2.length1 <= s1.length <= 30s1ands2consist of lowercase English letters.
To solve the Scramble String problem, we can use dynamic programming (DP) to efficiently check if s2 is a scrambled string of s1. The problem can be understood as a recursive process where we split the strings at different positions and recursively check for possible scrambled configurations.
Approach:
We can define a recursive relationship where:
- If
s1ands2are identical, we returntrue. - If
s1ands2have different characters, we returnfalse. - If
s1ands2can be divided into two substrings, we need to check two conditions:- No Swap: The first part of
s1can match the first part ofs2and the second part ofs1matches the second part ofs2. - With Swap: The first part of
s1matches the second part ofs2and the second part ofs1matches the first part ofs2.
- No Swap: The first part of
Dynamic Programming Approach:
We can use memoization to store the results of subproblems to avoid redundant calculations. Specifically, we use a 3D DP table dp[i][j][len] where:
iandjrepresent the starting indices of the substrings ins1ands2.lenis the length of the substring.dp[i][j][len] = trueif the substrings1[i..i+len-1]can be scrambled to forms2[j..j+len-1].
Code:
public class Solution {
public bool IsScramble(string s1, string s2) {
if (s1.Length != s2.Length) return false; // If lengths are different, return false
// Initialize the DP array for memoization
int n = s1.Length;
bool[,,] dp = new bool[n, n, n + 1];
// Base case: when the substring lengths are 1, we only need to check the characters
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i, j, 1] = s1[i] == s2[j];
}
}
// Check for all possible lengths of the substrings from 2 to n
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
for (int j = 0; j <= n - len; j++) {
for (int k = 1; k < len; k++) {
// No Swap case: s1[i..i+k-1] and s1[i+k..i+len-1] match s2[j..j+k-1] and s2[j+k..j+len-1]
if (dp[i, j, k] && dp[i + k, j + k, len - k]) {
dp[i, j, len] = true;
break;
}
// With Swap case: s1[i..i+k-1] and s1[i+k..i+len-1] match s2[j+len-k..j+len-1] and s2[j..j+k-1]
if (dp[i, j + len - k, k] && dp[i + k, j, len - k]) {
dp[i, j, len] = true;
break;
}
}
}
}
}
// Return whether the entire string can be scrambled from s1 to s2
return dp[0, 0, n];
}
}
Explanation:
-
Base Case:
- If the substrings have a length of
1, we simply check if the characters at those positions are equal, i.e.,s1[i] == s2[j].
- If the substrings have a length of
-
Filling the DP Table:
- For each possible substring length (
len), starting from 2 up to the length of the stringn, we check all possible starting indicesiandjfors1ands2. - For each combination of substrings of length
len, we try both the "no swap" and "with swap" conditions:- No Swap: The first
kcharacters ofs1should match the firstkcharacters ofs2, and the remaining characters should also match. - With Swap: The first
kcharacters ofs1should match the lastkcharacters ofs2, and the remaining characters should also match.
- No Swap: The first
- For each possible substring length (
-
Final Result:
- The result of whether
s1can be scrambled tos2is stored indp[0, 0, n]wherenis the length of the strings.
- The result of whether
Time Complexity:
- Time Complexity: , where
nis the length of the strings. This is because we have 3 loops:lenloop runs from 2 ton.iandjloops both run from 0 ton - len.- The inner loop (
k) runs up tolen.
- Space Complexity: for the 3D DP table.
Example Walkthrough:
For the input:
s1 = "great", s2 = "rgeat"
- The algorithm recursively checks whether
s1can be scrambled to forms2using both possible cases (with or without swap). - It iterates through all possible substring splits and applies the scramble check recursively, storing results in the DP table for optimization.
- Ultimately, it returns
truebecauses2is indeed a scrambled string ofs1.
Edge Cases:
- If the strings are identical (e.g.,
s1 = "a",s2 = "a"), the result istrue. - If the strings have different characters, the result is
false(e.g.,s1 = "abc",s2 = "def"). - The solution handles all edge cases within the given constraints (1 <= s1.length <= 30).
This dynamic programming solution is efficient and avoids redundant calculations by using memoization, making it suitable for the given constraints.
Another approach for solving the Scramble String problem that also involves recursion with memoization. The key idea in this approach is to use a recursive check for scramble relationships and utilize caching (memoization) to store already computed results for substrings, which helps in avoiding redundant recalculations.
Approach: Recursive with Memoization
In this approach, we define a recursive function to check if a substring of s1 can be scrambled to form a corresponding substring of s2. The recursion works as follows:
- Base Case: If the two strings are identical, they are trivially scrambled (return
true). - Character Frequency Check: If the two strings have different character frequencies (i.e., the same characters in a different order), then
s2cannot be a scrambled version ofs1, and we returnfalse. - Recursive Step:
- We recursively divide the strings into two parts at each possible position
kand check:- No Swap Case: Whether the first
kcharacters ofs1can scramble to the firstkcharacters ofs2, and the remaining part ofs1can scramble to the remaining part ofs2. - Swap Case: Whether the first
kcharacters ofs1can scramble to the lastkcharacters ofs2, and the remaining part ofs1can scramble to the first part ofs2.
- No Swap Case: Whether the first
- We recursively divide the strings into two parts at each possible position
- Memoization: Store the results of the recursive calls for each substring combination to avoid redundant calculations.
Code:
public class Solution {
public bool IsScramble(string s1, string s2) {
// Memoization dictionary to store the results of subproblems
Dictionary<string, bool> memo = new Dictionary<string, bool>();
// Helper function for recursion with memoization
return IsScrambleHelper(s1, s2, memo);
}
private bool IsScrambleHelper(string s1, string s2, Dictionary<string, bool> memo) {
// If the two strings are identical, no need to scramble
if (s1 == s2) return true;
// If the lengths of the strings are different, they can't be scrambled
if (s1.Length != s2.Length) return false;
// Check if the result is already computed and stored in memo
string key = s1 + " " + s2;
if (memo.ContainsKey(key)) return memo[key];
// If the two strings have different character counts, return false
if (!IsSameCharacterCount(s1, s2)) {
memo[key] = false;
return false;
}
int n = s1.Length;
for (int i = 1; i < n; i++) {
// Check for both swap and no swap cases
if ((IsScrambleHelper(s1.Substring(0, i), s2.Substring(0, i), memo) &&
IsScrambleHelper(s1.Substring(i), s2.Substring(i), memo)) ||
(IsScrambleHelper(s1.Substring(0, i), s2.Substring(n - i), memo) &&
IsScrambleHelper(s1.Substring(i), s2.Substring(0, n - i), memo))) {
memo[key] = true;
return true;
}
}
// Store the result in memo for future use
memo[key] = false;
return false;
}
// Helper function to check if both strings have the same character count
private bool IsSameCharacterCount(string s1, string s2) {
int[] count = new int[26];
for (int i = 0; i < s1.Length; i++) {
count[s1[i] - 'a']++;
count[s2[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (count[i] != 0) return false;
}
return true;
}
}
Explanation:
- Memoization: A dictionary
memois used to store previously computed results for pairs of strings (s1,s2). This prevents redundant recursive calls and optimizes performance. - Base Case: If
s1ands2are the same string, returntrue. If their lengths differ, returnfalse. - Character Frequency Check: Before diving into recursion, we check if
s1ands2contain the same characters in the same frequency using theIsSameCharacterCounthelper function. If they don't, returnfalse. - Recursive Case: We recursively check for both the "no swap" and "swap" cases by splitting the strings at every possible position
i. If any of the cases returntrue, we conclude thats1can be scrambled to forms2. - Memoization Storage: If we find that a pair of substrings can't be scrambled to each other, we store the result
falsein thememodictionary. If we find that they can, we storetrue.
Time Complexity:
- Time Complexity: , where
nis the length of the string. This is because:- Each pair of substrings is checked and stored in the memo dictionary.
- For each substring pair, we check all possible splits.
- The recursive calls involve checking the same substring pairs multiple times, which is why memoization helps to avoid redundant work.
- Space Complexity: for the memo dictionary and the recursion stack.
Example Walkthrough:
For input s1 = "great" and s2 = "rgeat", the function proceeds as follows:
- The algorithm checks if
s1ands2have the same character counts. - It then recursively splits both strings at each possible position and checks both swap and non-swap configurations.
- The recursion will eventually find that
s1can be scrambled intos2, and returntrue.
Edge Cases:
- If
s1ands2are already equal, the algorithm returnstruewithout further recursion. - If
s1ands2have different character sets, the result will befalseimmediately.
This approach is effective, and with memoization, the solution avoids recalculating the same results multiple times, leading to better performance compared to the naive recursive approach.